Initial commit
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# Copy this file to .env and fill in your library's values.
|
||||
# .env is gitignored -- never commit real credentials.
|
||||
|
||||
# Hostname only, no scheme/path, e.g. osceola.librarycatalog.info
|
||||
POLARIS_SERVER=
|
||||
|
||||
# Customer site domain segment in the API URI (default is usually "polaris")
|
||||
POLARIS_SITE_DOMAIN=polaris
|
||||
|
||||
# Logged-on organization ID and workstation ID (integers, from Polaris)
|
||||
POLARIS_ORGANIZATION_ID=
|
||||
POLARIS_WORKSTATION_ID=
|
||||
|
||||
# ISO 639-2 language code used in the API URI
|
||||
POLARIS_LANGUAGE=eng
|
||||
|
||||
# Product ID for the URI. Notices=7 is documented as correct for this endpoint;
|
||||
# if that 400s, try 19 (PolarisApplicationServices) instead.
|
||||
POLARIS_PRODUCT_ID=7
|
||||
|
||||
# Staff login. Username MUST include the domain, e.g. MYLIB\Aladdin
|
||||
POLARIS_USERNAME=
|
||||
POLARIS_PASSWORD=
|
||||
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
@@ -0,0 +1,64 @@
|
||||
# Remote Poster
|
||||
|
||||
Posts notices to a Polaris ILS database via the Polaris Leap (PAPI) API.
|
||||
|
||||
## Setup
|
||||
|
||||
1. (Recommended) create and activate a virtual environment, then install dependencies:
|
||||
```
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
On macOS with Homebrew Python you may get an "externally managed environment" error if you
|
||||
skip the venv and `pip install` directly — the venv avoids that.
|
||||
2. Copy `.env.example` to `.env` and fill in your server, org/workstation IDs, and staff login.
|
||||
`.env` is gitignored, so real credentials never get committed.
|
||||
|
||||
- `POLARIS_USERNAME` **must** include the domain, e.g. `MYLIB\Aladdin`.
|
||||
- `POLARIS_ORGANIZATION_ID` should match the `OrganizationID` your staff login actually
|
||||
belongs to (visible in the `PolarisUser` object returned on successful auth) — it's your
|
||||
*logged-on* org context, not the list of orgs you're posting notices to.
|
||||
- `POLARIS_PRODUCT_ID` defaults to `7` (Notices). If calls fail, `19`
|
||||
(PolarisApplicationServices) is the confirmed working alternative — see Notes below.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
python post_notices.py --notice-type 2 --org-ids 3,4,5,6,7,8
|
||||
```
|
||||
|
||||
- `--notice-type`: the Notification Type ID to post (from your Polaris system tables).
|
||||
- `--org-ids`: comma-separated Organization IDs to post notices *for* (the branches whose
|
||||
notices go out) — separate from `POLARIS_ORGANIZATION_ID` in `.env`, which is your login context.
|
||||
- `--delivery-option`: optional, defaults to `1` (Mail) — currently the only value Polaris accepts.
|
||||
|
||||
On success it prints a confirmation. On failure (bad credentials, invalid IDs, etc.) it prints
|
||||
the error from Polaris and exits non-zero.
|
||||
|
||||
Every run sends real notices to real patrons — there's no dry-run mode. Double-check
|
||||
`--notice-type` and `--org-ids` before running.
|
||||
|
||||
## Notes on the API (things the published docs get wrong or omit)
|
||||
|
||||
While wiring this up against Osceola's live server, two things in the reference doc
|
||||
(`docs/polaris-papi-reference.md`) turned out to be incomplete or misleading:
|
||||
|
||||
1. **The staff authentication URL is shorter than every other endpoint's.** Most PAPI routes
|
||||
follow `/api/{version}/{language}/{productId}/{siteDomain}/{organizationId}/{workstationId}/...`,
|
||||
but `authentication/staffuser` only uses `/api/{version}/{language}/{productId}/authentication/staffuser`
|
||||
— no `siteDomain`, `organizationId`, or `workstationId`. This makes sense in hindsight (those
|
||||
three describe a *logged-on* session, and you're not logged on yet when authenticating), but
|
||||
it isn't spelled out anywhere in the rendered help page — it's only visible as a `title="..."`
|
||||
tooltip attribute in the raw HTML of
|
||||
`polaris.applicationservices/help/authentication/post_staffuser`, which a normal page read
|
||||
(rendered text, or an AI summary of the page) won't surface. `polaris_client.py` builds this
|
||||
URL separately from every other call (`self.auth_url` vs `self.base_url`).
|
||||
2. **`productId`**: `19` (PolarisApplicationServices) is confirmed working against the live
|
||||
server; `7` (Notices) was the doc's best guess but untested — worth trying if `19` ever stops
|
||||
working, but don't be surprised if it 400s/404s.
|
||||
|
||||
If you hit a `404` with an empty response body on any call, that almost always means the URL
|
||||
path itself doesn't match a route (wrong segment count, wrong product ID, etc.) rather than a
|
||||
credentials problem — Polaris's own application-level errors come back as JSON with a
|
||||
`Message`/`ErrorCode`, not an empty body.
|
||||
@@ -0,0 +1,310 @@
|
||||
# Polaris.ApplicationServices (PAPI) API Reference
|
||||
|
||||
Source: Osceola Library System's Polaris.ApplicationServices help docs
|
||||
(`https://osceola.librarycatalog.info/polaris.applicationservices/help/...`)
|
||||
Compiled for internal reference to support scripting against the Notices endpoint.
|
||||
|
||||
---
|
||||
|
||||
## 1. The API — Basics
|
||||
|
||||
- Base API version: `v1` (only supported version)
|
||||
- Status check URL: `https://[your server name]/Polaris.ApplicationServices/api`
|
||||
Returns a JSON blob with `ServiceName`, `Version`, `Major`, `Minor`, `Build`, `Revision`.
|
||||
|
||||
### URI structure
|
||||
|
||||
All URIs (other than API status and staff authentication) include these route segments, in order:
|
||||
|
||||
```
|
||||
/api/{version}/{language}/{productId}/{siteDomain}/{organizationId}/{workstationId}/...
|
||||
```
|
||||
|
||||
| Segment | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `version` | string | Yes | `"v1"` |
|
||||
| `language` | string | Yes | ISO 639-2 code — `eng`, `spa`, `fre`, `chi`, `ara`, `fas`, `hat`, `haw`, `kor`, `rus`, `vie`, etc. |
|
||||
| `productId` | integer | Yes | See product ID table below |
|
||||
| `siteDomain` | string | Yes | Customer site domain (default is `"polaris"`) |
|
||||
| `organizationId` | integer | Yes | Logged-on organization ID |
|
||||
| `workstationId` | integer | Yes | Logged-on workstation ID |
|
||||
|
||||
Example full URI:
|
||||
```
|
||||
/api/v1/eng/19/polaris/1/1/patrons/{id}/basicdata
|
||||
```
|
||||
Docs often show this in shorthand as `/api/.../patrons/{id}/basicdata`.
|
||||
|
||||
### Product IDs
|
||||
|
||||
| Value | Product |
|
||||
|---|---|
|
||||
| 0 | All |
|
||||
| 1 | PowerPAC |
|
||||
| 2 | ChildrensPAC |
|
||||
| 3 | ActivePAC |
|
||||
| 4 | ExpressCheck |
|
||||
| 5 | InboundTelephony |
|
||||
| 6 | OutboundTelephony |
|
||||
| 7 | Notices |
|
||||
| 8 | ILL |
|
||||
| 9 | PolarisFusion |
|
||||
| 10 | AcquisitionsExchange |
|
||||
| 11 | MobilePAC |
|
||||
| 12 | SIPService |
|
||||
| 13 | NCIPService |
|
||||
| 14 | ERMSPortal |
|
||||
| 15 | Receipts |
|
||||
| 16 | ContentXChange |
|
||||
| 17 | PolarisImport |
|
||||
| 18 | PolarisAPIConsumerService |
|
||||
| 19 | PolarisApplicationServices |
|
||||
| 20 | StaffWebClient |
|
||||
| 21 | StaffClient |
|
||||
| 22 | PolarisDatabase |
|
||||
| 23 | SystemOutput |
|
||||
| 24 | Vega |
|
||||
|
||||
> Since we're calling the Notices endpoint, `productId = 7` (Notices) is likely the semantically "correct" one to use, though `19` (PolarisApplicationServices) may also work depending on how the server is configured — worth confirming against a working example if you have one.
|
||||
|
||||
### HTTP response codes
|
||||
|
||||
| Code | Status | Notes |
|
||||
|---|---|---|
|
||||
| 200 | OK | Success |
|
||||
| 201 | Created | Returns created object |
|
||||
| 204 | No Content | Successful deletion |
|
||||
| 400 | Bad Request | Validation errors, malformed params, etc. Body includes `Message`/`MessageDetail`, sometimes `ErrorCode`. |
|
||||
| 401 | Unauthorized | `{"ErrorCode":10001,"Message":"User authentication failed."}` |
|
||||
| 403 | Forbidden | Permission not granted; response includes override info (see Security section) |
|
||||
| 404 | Not Found | Either "no matching route" or "record not found" (e.g. `ErrorCode 60027`) |
|
||||
| 405 | Method Not Allowed | Wrong HTTP verb for the resource |
|
||||
| 409 | Conflict | Object lock (`ErrorCode` 70000–79999) or secured record (`ErrorCode` 10004) |
|
||||
| 411 | Length Required | Missing `Content-Length` |
|
||||
| 422 | Unprocessable Entity | Body isn't valid JSON |
|
||||
| 500+ | Server Error | Unhandled exception |
|
||||
|
||||
---
|
||||
|
||||
## 2. Security and Authentication
|
||||
|
||||
Three supported auth mechanisms:
|
||||
|
||||
1. **HTTP Basic Authentication + Access Token/Secret** (the standard flow)
|
||||
2. **ASP.NET Forms Authentication** (cookie-based, browser/JS clients on same domain)
|
||||
3. **Bearer Token via OAuth2** (OpenID Connect + PKCE)
|
||||
|
||||
Plus: **Proxy-Authorization** for privilege overrides.
|
||||
|
||||
### 2.1 Basic Auth → Access Token/Secret (primary flow for scripting)
|
||||
|
||||
Step 1 — Call the staff auth endpoint with HTTP Basic Auth:
|
||||
|
||||
```
|
||||
POST /api/.../authentication/staffuser
|
||||
Authorization: Basic <base64(DOMAIN\username:password)>
|
||||
```
|
||||
|
||||
- Username **must include the domain**, e.g. `MYLIB\Aladdin`.
|
||||
- Combine as `"DOMAIN\username:password"`, then base64-encode the whole string.
|
||||
- No request body.
|
||||
|
||||
Step 2 — Response contains an `AccessToken` and `AccessSecret`:
|
||||
|
||||
```json
|
||||
{
|
||||
"SiteDomain": "polaris",
|
||||
"UserDomain": "iii.com",
|
||||
"AccessToken": "NXmeihFv2kq6meg3EdYoenv2VagJrPHs",
|
||||
"AccessSecret": "odXCBZuhXBkbwSo4",
|
||||
"AuthExpDate": "2013-03-26T10:41:11.103",
|
||||
"PolarisUser": {
|
||||
"PolarisUserID": 923,
|
||||
"OrganizationID": 3,
|
||||
"Name": "Young",
|
||||
"BranchID": null,
|
||||
"Enabled": true,
|
||||
"CreatorID": 895,
|
||||
"ModifierID": null,
|
||||
"CreationDate": "2011-02-16T20:28:16.177",
|
||||
"ModificationDate": null
|
||||
},
|
||||
"ERMSNetworkAddress": "young-lt2.polarislibrary.com",
|
||||
"DataSource": "RD-POLARIS"
|
||||
}
|
||||
```
|
||||
|
||||
Step 3 — Use the token/secret on all subsequent calls, in a custom `PAS` auth scheme (not base64-encoded):
|
||||
|
||||
```
|
||||
Authorization: PAS {siteDomain}:{AccessToken}:{AccessSecret}
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
Authorization: PAS polaris:NXmeihFv2kq6meg3EdYoenv2VagJrPHs:odXCBZuhXBkbwSo4
|
||||
```
|
||||
|
||||
### 2.2 Bearer Token (OAuth2) variant of staff auth
|
||||
|
||||
```
|
||||
POST /api/.../authentication/staffuser/oauth
|
||||
Authorization: Bearer <JWT access token>
|
||||
```
|
||||
|
||||
- The JWT's `upn` claim must be `username@domain` format and match a Polaris staff user.
|
||||
- Response shape is identical to the Basic Auth flow (still returns `AccessToken`/`AccessSecret` to use for follow-on calls).
|
||||
|
||||
### 2.3 API Key Authentication (alternative, no token/secret round-trip)
|
||||
|
||||
```
|
||||
POST /api/.../authentication/staffuser
|
||||
x-api-key: <your API key>
|
||||
```
|
||||
|
||||
- No `Authorization` header — the key goes in the `x-api-key` header.
|
||||
- Response does **not** include `AccessToken`/`AccessSecret` (they're blank strings) — the API key itself is presumably reused directly on subsequent calls (docs don't show that call shape explicitly, so confirm against a working example).
|
||||
|
||||
```json
|
||||
{
|
||||
"SiteDomain": "",
|
||||
"UserDomain": "",
|
||||
"AccessToken": "",
|
||||
"AccessSecret": "",
|
||||
"AuthExpDate": null,
|
||||
"PolarisUser": {
|
||||
"PolarisUserID": 1,
|
||||
"OrganizationID": 5,
|
||||
"Name": "PolarisExec",
|
||||
"BranchID": null,
|
||||
"Enabled": true,
|
||||
"CreatorID": 1,
|
||||
"ModifierID": 1,
|
||||
"CreationDate": null,
|
||||
"ModificationDate": "2020-12-11T10:28:20.69-05:00"
|
||||
},
|
||||
"ERMSNetworkAddress": "clvitn-pc197yjl.polarislibrary.com",
|
||||
"DataSource": "CLVITN-PC197YJL",
|
||||
"SiteCode": "MIS/"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Privilege overrides (403 handling)
|
||||
|
||||
If a call returns `403 Forbidden` because the authenticated user lacks a permission, you can supply override credentials via:
|
||||
|
||||
- `Proxy-Authorization` header, **or**
|
||||
- `Polaris-Override-Authorization` custom header (supports multiple overrides per call)
|
||||
|
||||
Format (before base64 encoding):
|
||||
```
|
||||
username:password:permissionid:permissionownerid
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
Proxy-Authorization: Basic cG9sYXJpc2V4ZWNAZG9tYWluOnBhc3N3b3JkOjgzOjEwMw==
|
||||
```
|
||||
|
||||
Multiple overrides via repeated headers or a comma-delimited list of `Basic ...` values in one header.
|
||||
|
||||
**Note:** the primary `Authorization` header (token/secret) is still required alongside any override header.
|
||||
|
||||
---
|
||||
|
||||
## 3. HTTP Headers Cheat Sheet
|
||||
|
||||
**GET requests** — set `Accept`:
|
||||
```
|
||||
Accept: application/json
|
||||
```
|
||||
(also valid: `text/json`, `image/jpg` for binary/image responses)
|
||||
|
||||
**PATCH / POST / PUT requests** — set `Content-Type`:
|
||||
```
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**Auth headers**:
|
||||
```
|
||||
Authorization: PAS {siteDomain}:{AccessToken}:{AccessSecret}
|
||||
Proxy-Authorization: Basic <base64 override>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Notices — POST (the endpoint we care about)
|
||||
|
||||
### Post notices to the database
|
||||
|
||||
```
|
||||
POST /api/.../notices/post?noticeType={notificationtypeid}&deliveryOption={deliveryoptionid}
|
||||
```
|
||||
|
||||
#### Query parameters
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `noticeType` | integer | Yes | ID of the Notification Type |
|
||||
| `deliveryOption` | integer | Yes | ID of the Delivery Option. **Only valid value is Mail (`1`).** |
|
||||
|
||||
#### Request body
|
||||
|
||||
A JSON string containing a comma-separated list of Organization IDs:
|
||||
|
||||
```json
|
||||
"3,5,7"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
Returns a plain boolean.
|
||||
|
||||
- Success: `true`
|
||||
- Failure: `false`
|
||||
|
||||
#### HTTP response codes
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| 200 | OK. Success |
|
||||
| 400 | Failure — bad request, invalid `noticeType`, or invalid `deliveryOption` |
|
||||
|
||||
#### Notes / open questions for implementation
|
||||
|
||||
- The docs don't enumerate valid `noticeType` (Notification Type) IDs on this page — that's presumably in Polaris's system tables or the "System Administration" section elsewhere in the docs (e.g. Parameters and Profiles, or a lookup table). Worth checking the Polaris database (`Polaris.SystemNotifications` or similar) or asking the vendor if you need the full list.
|
||||
- `deliveryOption` is documented as only accepting `1` (Mail) right now, despite the API allowing an integer — so don't expect e.g. email/SMS options to work here even if other DeliveryOptions endpoints list more.
|
||||
- The response being a bare `true`/`false` (not an object) means your client code should parse the raw JSON boolean rather than expecting a structured object — easy to trip up if you assume every endpoint returns a DTO.
|
||||
- `productId` for this call is most likely `7` (Notices) per the product ID table — worth confirming with a test call.
|
||||
|
||||
---
|
||||
|
||||
## 5. Corrections confirmed against a live server (Osceola)
|
||||
|
||||
While building `post_notices.py` against Osceola's live Polaris.ApplicationServices instance,
|
||||
two things in this doc turned out to be wrong or incomplete:
|
||||
|
||||
- **Staff authentication URL is shorter than section 1 implies.** The "all URIs other than API
|
||||
status and staff authentication" caveat in section 1 undersells how different the auth route
|
||||
is. It is **not** `/api/{version}/{language}/{productId}/{siteDomain}/{organizationId}/{workstationId}/authentication/staffuser`
|
||||
— it's just `/api/{version}/{language}/{productId}/authentication/staffuser`, with no
|
||||
`siteDomain`, `organizationId`, or `workstationId` segments at all. Calling the full-length
|
||||
URL returns a `404` with an **empty body** (a routing miss, not an app-level error — Polaris's
|
||||
real error responses come back as JSON with `Message`/`ErrorCode`). This exact URL shape isn't
|
||||
visible anywhere in the rendered help page text; it only shows up as a `title="{version}/{language}/{productId}"`
|
||||
attribute on the `<span>` wrapping the "..." in `POST /api/.../authentication/staffuser`, i.e.
|
||||
you have to view-source the page (`polaris.applicationservices/help/authentication/post_staffuser`)
|
||||
to find it — a plain fetch or rendered read of the page won't surface it.
|
||||
- **`productId = 19`** (PolarisApplicationServices) is confirmed working end-to-end (auth +
|
||||
`notices/post`) against Osceola's server. `productId = 7` (Notices) — this doc's original best
|
||||
guess — was not confirmed; it may still be correct, but wasn't tested.
|
||||
|
||||
---
|
||||
|
||||
## 6. Gaps in this doc (pages that returned no additional content)
|
||||
|
||||
Two pages in the doc set — **Common Response Structures** and **Collections** (both under "Introduction") — returned only their page heading with no body content when fetched. This might mean:
|
||||
- The content is loaded client-side via JavaScript that a simple page fetch won't execute, or
|
||||
- Those particular pages are thin/placeholder pages in this Polaris version.
|
||||
|
||||
If you need the actual content of those two pages (e.g. for handling paginated list responses in the `notices` or other endpoints), it may be worth viewing them directly in a browser to check whether there's dynamically-loaded content, since a plain HTTP fetch didn't surface anything under those headings.
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Minimal client for the Polaris Leap (PAPI) Notices endpoint."""
|
||||
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class PolarisAuthError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PolarisRequestError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PolarisClient:
|
||||
def __init__(
|
||||
self,
|
||||
server,
|
||||
site_domain,
|
||||
organization_id,
|
||||
workstation_id,
|
||||
language="eng",
|
||||
product_id=7,
|
||||
):
|
||||
self.auth_url = f"https://{server}/Polaris.ApplicationServices/api/v1/{language}/{product_id}/authentication/staffuser"
|
||||
self.base_url = f"https://{server}/Polaris.ApplicationServices/api/v1/{language}/{product_id}/{site_domain}/{organization_id}/{workstation_id}"
|
||||
self.site_domain = site_domain
|
||||
self.access_token = None
|
||||
self.access_secret = None
|
||||
|
||||
def authenticate(self, username, password):
|
||||
"""Exchange a staff username/password for an AccessToken/AccessSecret.
|
||||
|
||||
Username must include the domain, e.g. MYLIB\\Aladdin.
|
||||
|
||||
Note: per the live API help page (view-source, not the rendered page --
|
||||
it's a tooltip on the "..." in the route), the staff-authentication route
|
||||
only includes {version}/{language}/{productId} -- no siteDomain,
|
||||
organizationId, or workstationId, since those describe a *logged-on*
|
||||
session and you're not logged on yet at this point.
|
||||
"""
|
||||
response = requests.post(
|
||||
self.auth_url,
|
||||
auth=(username, password),
|
||||
headers={"Accept": "application/json"},
|
||||
)
|
||||
if response.status_code == 401:
|
||||
raise PolarisAuthError("Authentication failed: check username/password.")
|
||||
if not response.ok:
|
||||
raise PolarisAuthError(
|
||||
f"Authentication failed ({response.status_code}): {response.text}"
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
self.access_token = data["AccessToken"]
|
||||
self.access_secret = data["AccessSecret"]
|
||||
return data
|
||||
|
||||
def _auth_header(self):
|
||||
if not self.access_token or not self.access_secret:
|
||||
raise PolarisAuthError("Not authenticated yet -- call authenticate() first.")
|
||||
return f"PAS {self.site_domain}:{self.access_token}:{self.access_secret}"
|
||||
|
||||
def post_notices(self, notice_type, organization_ids, delivery_option=1):
|
||||
"""POST notices for the given notice type to the given org IDs.
|
||||
|
||||
organization_ids: an iterable/list of integer org IDs.
|
||||
delivery_option: only 1 (Mail) is currently supported by the API.
|
||||
|
||||
Returns True/False as reported by Polaris.
|
||||
"""
|
||||
org_ids_csv = ",".join(str(oid) for oid in organization_ids)
|
||||
|
||||
response = requests.post(
|
||||
f"{self.base_url}/notices/post",
|
||||
params={"noticeType": notice_type, "deliveryOption": delivery_option},
|
||||
headers={
|
||||
"Authorization": self._auth_header(),
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
data=json.dumps(org_ids_csv),
|
||||
)
|
||||
if not response.ok:
|
||||
raise PolarisRequestError(
|
||||
f"Notice post failed ({response.status_code}): {response.text}"
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
if result is not True:
|
||||
raise PolarisRequestError(
|
||||
f"Polaris reported failure (returned {result!r}) for noticeType={notice_type}, "
|
||||
f"organizationIds={org_ids_csv}"
|
||||
)
|
||||
return result
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI for posting Polaris notices via the Leap (PAPI) API.
|
||||
|
||||
Usage:
|
||||
python post_notices.py --notice-type 8 --org-ids 3,5,7
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from polaris_client import PolarisAuthError, PolarisClient, PolarisRequestError
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Post notices to Polaris via the Leap API.")
|
||||
parser.add_argument(
|
||||
"--notice-type",
|
||||
type=int,
|
||||
required=True,
|
||||
help="Notification Type ID to post (see your Polaris system tables).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--org-ids",
|
||||
required=True,
|
||||
help="Comma-separated list of Organization IDs, e.g. 3,5,7",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--delivery-option",
|
||||
type=int,
|
||||
default=1,
|
||||
help="Delivery Option ID. Only 1 (Mail) is currently supported by Polaris.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
load_dotenv()
|
||||
args = parse_args()
|
||||
|
||||
required_env = [
|
||||
"POLARIS_SERVER",
|
||||
"POLARIS_SITE_DOMAIN",
|
||||
"POLARIS_ORGANIZATION_ID",
|
||||
"POLARIS_WORKSTATION_ID",
|
||||
"POLARIS_USERNAME",
|
||||
"POLARIS_PASSWORD",
|
||||
]
|
||||
missing = [name for name in required_env if not os.environ.get(name)]
|
||||
if missing:
|
||||
print(f"Missing required .env values: {', '.join(missing)}", file=sys.stderr)
|
||||
print("Copy .env.example to .env and fill it in.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
org_ids = [int(oid.strip()) for oid in args.org_ids.split(",") if oid.strip()]
|
||||
|
||||
client = PolarisClient(
|
||||
server=os.environ["POLARIS_SERVER"],
|
||||
site_domain=os.environ["POLARIS_SITE_DOMAIN"],
|
||||
organization_id=os.environ["POLARIS_ORGANIZATION_ID"],
|
||||
workstation_id=os.environ["POLARIS_WORKSTATION_ID"],
|
||||
language=os.environ.get("POLARIS_LANGUAGE", "eng"),
|
||||
product_id=os.environ.get("POLARIS_PRODUCT_ID", "7"),
|
||||
)
|
||||
|
||||
try:
|
||||
client.authenticate(os.environ["POLARIS_USERNAME"], os.environ["POLARIS_PASSWORD"])
|
||||
client.post_notices(args.notice_type, org_ids, args.delivery_option)
|
||||
except (PolarisAuthError, PolarisRequestError) as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(
|
||||
f"Notices posted successfully: noticeType={args.notice_type}, "
|
||||
f"orgIds={org_ids}, deliveryOption={args.delivery_option}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,2 @@
|
||||
requests>=2.31
|
||||
python-dotenv>=1.0
|
||||
Reference in New Issue
Block a user