forked from CMS/memberPlumbing
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
66853e1156 | |||
363be0ba8c | |||
d8b3958c87 | |||
68b4b10c51 | |||
88b2610513 | |||
3f17cd9ec2 | |||
5c53f7f88c | |||
97c4dbc1ee | |||
3595a24d85 | |||
c1430e2f9a | |||
a5a787e0f7 | |||
6b7194c15a | |||
855f9b652d | |||
69bcb71091 | |||
63bd8efaf2 | |||
af6ecb2864 | |||
ce2a0f4c1d | |||
995b6f9763 | |||
f94a27699c | |||
34539eb630 | |||
3849aca918 | |||
cfccc433dd | |||
a2dd00f414 | |||
5a39c5cae9 | |||
7a22f43ccf |
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,7 +1,3 @@
|
|||||||
/.coverage
|
|
||||||
/.mypy_cache/
|
|
||||||
/memberplumbing.egg-info/
|
|
||||||
/venv/
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
/venv/
|
||||||
/config.yaml
|
/config.yaml
|
||||||
|
20
README.md
20
README.md
@ -4,7 +4,7 @@ This repo contains a set of scripts to sync data around for the Claremont MakerS
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
This project uses [Poetry](https://python-poetry.org/) for dependency management. Typical usage is first running `poetry install` to create a virtualenv and install dependencies, then running `poetry run bin/<script>` to start a specific script.
|
This project uses [Poetry](https://python-poetry.org/) for dependency management. Typical usage is first running `poetry install` to create a virtualenv and install dependencies, then running `poetry run <script>` to start a specific script.
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ Many of the scripts use data from a `config.yaml` in the current working directo
|
|||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
The primary entry points have scripts in [`bin`](./bin/). They assume that memberPlumbing is in `PYTHONPATH`, which can either be done with `poetry` or manually set.
|
The primary entry points have scripts entries (`tool.poetry.scripts`) in [`pyproject.toml`](./pyproject.toml). They assume that they are being run from a module, so must be run with `poetry run <script>` or `python -m memberPlumbing.<script>`.
|
||||||
|
|
||||||
### `doorUpdater`
|
### `doorUpdater`
|
||||||
|
|
||||||
@ -26,6 +26,10 @@ Retrieves member information from MembershipWorks and pushes it out to [UCS](htt
|
|||||||
|
|
||||||
Retrieves account and transaction information from MembershipWorks, and pushes it to a MariaDB database for use in other projects. Schemas are defined with [peewee](peewee-orm.com) in [`memberPlumbing/mw_models.py`](./memberPlumbing/mw_models.py).
|
Retrieves account and transaction information from MembershipWorks, and pushes it to a MariaDB database for use in other projects. Schemas are defined with [peewee](peewee-orm.com) in [`memberPlumbing/mw_models.py`](./memberPlumbing/mw_models.py).
|
||||||
|
|
||||||
|
### `upcomingEvents`
|
||||||
|
|
||||||
|
Retrieves upcoming events from MembershipWorks and formats them for a WordPress post.
|
||||||
|
|
||||||
### `hidEvents`
|
### `hidEvents`
|
||||||
|
|
||||||
Retrieves events from the HID Evo Solo door controllers, and pushes them to a SQL database.
|
Retrieves events from the HID Evo Solo door controllers, and pushes them to a SQL database.
|
||||||
@ -33,15 +37,3 @@ Retrieves events from the HID Evo Solo door controllers, and pushes them to a SQ
|
|||||||
## Systemd
|
## Systemd
|
||||||
|
|
||||||
There are systemd units in the [`systemd`](./systemd/) folder, which can be used to run the various scripts regularly.
|
There are systemd units in the [`systemd`](./systemd/) folder, which can be used to run the various scripts regularly.
|
||||||
|
|
||||||
## SSL Certificates
|
|
||||||
|
|
||||||
The HID Evo Solo door controllers we use have a self-signed certificate, which is included in this repo as [`hidglobal.com.pem`](./hidglobal.com.pem).
|
|
||||||
|
|
||||||
If you need to use a different certificate, you can either download it with your browser or the following command (replacing `SERVER` by the address of the door):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
openssl s_client -connect SERVER:443 </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > example.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
Then set `doorControllerCA_BUNDLE` in `config.yaml` to the path to the created pem file. If your doors have different certificates, (which ours annoyingly don't), you will need to concatenate the certificates together into a single file.
|
|
||||||
|
@ -6,8 +6,6 @@ doorControllers:
|
|||||||
Wood Shop Rear: {ip: 172.18.51.15, access: Wood Shop}
|
Wood Shop Rear: {ip: 172.18.51.15, access: Wood Shop}
|
||||||
Storage Closet: {ip: 172.18.51.16, access: Storage Closet}
|
Storage Closet: {ip: 172.18.51.16, access: Storage Closet}
|
||||||
|
|
||||||
doorControllerCA_BUNDLE: "hidglobal.com.pem"
|
|
||||||
|
|
||||||
# {member type: door schedule}
|
# {member type: door schedule}
|
||||||
memberLevels:
|
memberLevels:
|
||||||
CMS Staff: 7x24
|
CMS Staff: 7x24
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIID9TCCAt2gAwIBAgIJAPBotjnyfGu6MA0GCSqGSIb3DQEBCwUAMIGQMQswCQYD
|
|
||||||
VQQGEwJVUzERMA8GA1UECAwIQ29sb3JhZG8xFDASBgNVBAcMC1dlc3RtaW5zdGVy
|
|
||||||
MQwwCgYDVQQKDANISUQxDDAKBgNVBAsMA05BUzEWMBQGA1UEAwwNaGlkZ2xvYmFs
|
|
||||||
LmNvbTEkMCIGCSqGSIb3DQEJARYVc3VwcG9ydEBoaWRnbG9iYWwuY29tMB4XDTE5
|
|
||||||
MDcyMjEwMTQxMFoXDTI5MDcxOTEwMTQxMFowgZAxCzAJBgNVBAYTAlVTMREwDwYD
|
|
||||||
VQQIDAhDb2xvcmFkbzEUMBIGA1UEBwwLV2VzdG1pbnN0ZXIxDDAKBgNVBAoMA0hJ
|
|
||||||
RDEMMAoGA1UECwwDTkFTMRYwFAYDVQQDDA1oaWRnbG9iYWwuY29tMSQwIgYJKoZI
|
|
||||||
hvcNAQkBFhVzdXBwb3J0QGhpZGdsb2JhbC5jb20wggEiMA0GCSqGSIb3DQEBAQUA
|
|
||||||
A4IBDwAwggEKAoIBAQDSibuXB9Tn0EdwL2jDig26s/b1D9SX5B4xnZM+xZ4/mE6U
|
|
||||||
Meg5xbiTSMiWqtoSMVxG1WJDxogJWxCgZis2qk3AG89PBarg17pBmxPLYyCLricx
|
|
||||||
alyNvTJBxYgA/zKagPof6h6UqKOkhsW9qvulEmPe+TKk47pmlZXe+v+1A6PQDY5B
|
|
||||||
Y3MtqE23cnZ5nBTVanFAc1vbokMXUCCtRvE1Y/KhvuaJr2VjOSJ/KV3vcdTSCLGc
|
|
||||||
W9/n/Fv8udvI/eIkoPNpCUwngm8j3Aa7qN/OSg3SvVvBcl/Ykc08STSyZPMJGBaR
|
|
||||||
EuUcAraEBZbUDOCinDS488jKVHAXhrnvzzi7RMlhAgMBAAGjUDBOMB0GA1UdDgQW
|
|
||||||
BBSvOzxCjQi86ZUsuW0o4aa7GNAeSTAfBgNVHSMEGDAWgBSvOzxCjQi86ZUsuW0o
|
|
||||||
4aa7GNAeSTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA/8FCv6x8v
|
|
||||||
PHm2Ya/hbZ/S3amdl7/E1illeApNRZodTGCn/rlVSGanCfWYzEY2naHiDC2ImhZq
|
|
||||||
NkHK9uvUtxaVQFq5VN6WQMo351J78LLfcoqpKOLGX3b9byFvrw7WporZx3C7yL1U
|
|
||||||
LS3oxI/pgavxy1KbOIw/yl+QgV50vlfvQ7sKZ1E5YOrgWLP5nJ9OeEKRdsASJyZS
|
|
||||||
Jjl0k/eGaZreSvAZPmx4kaePfbi7DNDA+mNhSFygwt6AakjjVoF2xUZ1F+qwBtER
|
|
||||||
GPxdZWldywUYsdBRG1PPvBsMo9ME46HpPdXRIjMge8P01fsaMr/6H86ojWg9uJmH
|
|
||||||
cCKtiouo08hL
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -2,8 +2,9 @@ import csv
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import datetime
|
||||||
|
|
||||||
BASE_URL = "https://api.membershipworks.com/v1/"
|
BASE_URL = "https://api.membershipworks.com"
|
||||||
|
|
||||||
# extracted from `SF._is.crm` in https://cdn.membershipworks.com/all.js
|
# extracted from `SF._is.crm` in https://cdn.membershipworks.com/all.js
|
||||||
CRM = {
|
CRM = {
|
||||||
@ -72,21 +73,30 @@ class MembershipWorksRemoteError(Exception):
|
|||||||
|
|
||||||
class MembershipWorks:
|
class MembershipWorks:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.sess = requests.Session()
|
||||||
self.org_info = None
|
self.org_info = None
|
||||||
self.auth_token = None
|
self.auth_token = None
|
||||||
self.org_num = None
|
self.org_num = None
|
||||||
|
|
||||||
def login(self, username, password):
|
def login(self, username, password):
|
||||||
"""Authenticate against the membershipworks api"""
|
"""Authenticate against the membershipworks api"""
|
||||||
r = requests.post(
|
r = self.sess.post(
|
||||||
BASE_URL + "usr",
|
BASE_URL + "/v2/account/session",
|
||||||
data={"_st": "all", "eml": username, "org": "10000", "pwd": password},
|
data={"eml": username, "pwd": password},
|
||||||
|
headers={"X-Org": "10000"},
|
||||||
)
|
)
|
||||||
if r.status_code != 200 or "SF" not in r.json():
|
if r.status_code != 200 or "SF" not in r.json():
|
||||||
raise MembershipWorksRemoteError("login", r)
|
raise MembershipWorksRemoteError("login", r)
|
||||||
self.org_info = r.json()
|
self.org_info = r.json()
|
||||||
self.auth_token = self.org_info["SF"]
|
self.auth_token = self.org_info["SF"]
|
||||||
self.org_num = self.org_info["org"]
|
self.org_num = self.org_info["org"]
|
||||||
|
self.sess.headers.update(
|
||||||
|
{
|
||||||
|
"X-Org": str(self.org_num),
|
||||||
|
"X-Role": "admin",
|
||||||
|
"Authorization": "Bearer " + self.auth_token,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def _inject_auth(self, kwargs):
|
def _inject_auth(self, kwargs):
|
||||||
# TODO: should probably be a decorator or something
|
# TODO: should probably be a decorator or something
|
||||||
@ -97,12 +107,12 @@ class MembershipWorks:
|
|||||||
kwargs["params"] = {}
|
kwargs["params"] = {}
|
||||||
kwargs["params"]["SF"] = self.auth_token
|
kwargs["params"]["SF"] = self.auth_token
|
||||||
|
|
||||||
def _get(self, *args, **kwargs):
|
def _get_v1(self, *args, **kwargs):
|
||||||
self._inject_auth(kwargs)
|
self._inject_auth(kwargs)
|
||||||
# TODO: should probably do some error handling in here
|
# TODO: should probably do some error handling in here
|
||||||
return requests.get(*args, **kwargs)
|
return requests.get(*args, **kwargs)
|
||||||
|
|
||||||
def _post(self, *args, **kwargs):
|
def _post_v1(self, *args, **kwargs):
|
||||||
self._inject_auth(kwargs)
|
self._inject_auth(kwargs)
|
||||||
# TODO: should probably do some error handling in here
|
# TODO: should probably do some error handling in here
|
||||||
return requests.post(*args, **kwargs)
|
return requests.post(*args, **kwargs)
|
||||||
@ -141,32 +151,30 @@ class MembershipWorks:
|
|||||||
for dek in self.org_info["dek"]:
|
for dek in self.org_info["dek"]:
|
||||||
# TODO: there must be a better way. this is stupid
|
# TODO: there must be a better way. this is stupid
|
||||||
if dek["dek"] == 1:
|
if dek["dek"] == 1:
|
||||||
ret["folders"][dek["lbl"]] = dek["_id"]
|
ret["folders"][dek["lbl"]] = dek["did"]
|
||||||
elif "cur" in dek:
|
elif "cur" in dek:
|
||||||
ret["levels"][dek["lbl"]] = dek["_id"]
|
ret["levels"][dek["lbl"]] = dek["did"]
|
||||||
elif "mux" in dek:
|
elif "mux" in dek:
|
||||||
ret["addons"][dek["lbl"]] = dek["_id"]
|
ret["addons"][dek["lbl"]] = dek["did"]
|
||||||
else:
|
else:
|
||||||
ret["labels"][dek["lbl"]] = dek["_id"]
|
ret["labels"][dek["lbl"]] = dek["did"]
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def get_member_ids(self, folders):
|
def get_member_ids(self, folders):
|
||||||
folder_map = self._parse_flags()["folders"]
|
folder_map = self._parse_flags()["folders"]
|
||||||
|
|
||||||
r = self._get(
|
r = self.sess.get(
|
||||||
BASE_URL + "ylp",
|
BASE_URL + "/v2/accounts",
|
||||||
params={
|
params={"dek": ",".join([folder_map[f] for f in folders])},
|
||||||
"lbl": ",".join([folder_map[f] for f in folders]),
|
|
||||||
"org": self.org_num,
|
|
||||||
"var": "_id,nam,ctc",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if r.status_code != 200 or "usr" not in r.json():
|
if r.status_code != 200 or "usr" not in r.json():
|
||||||
raise MembershipWorksRemoteError("user listing", r)
|
raise MembershipWorksRemoteError("user listing", r)
|
||||||
|
|
||||||
# get list of member ID matching the search
|
# get list of member ID matching the search
|
||||||
return [user["uid"] for user in r.json()["usr"]]
|
# dedup with set() to work around people with alt uids
|
||||||
|
# TODO: figure out why people have alt uids
|
||||||
|
return set(user["uid"] for user in r.json()["usr"])
|
||||||
|
|
||||||
# TODO: has issues with aliasing header names:
|
# TODO: has issues with aliasing header names:
|
||||||
# ex: "Personal Studio Space" Label vs Membership Addon/Field
|
# ex: "Personal Studio Space" Label vs Membership Addon/Field
|
||||||
@ -179,8 +187,8 @@ class MembershipWorks:
|
|||||||
|
|
||||||
# get members CSV
|
# get members CSV
|
||||||
# TODO: maybe can just use previous get instead? would return JSON
|
# TODO: maybe can just use previous get instead? would return JSON
|
||||||
r = self._post(
|
r = self._post_v1(
|
||||||
BASE_URL + "csv",
|
BASE_URL + "/v1/csv",
|
||||||
data={
|
data={
|
||||||
"_rt": "946702800", # unknown
|
"_rt": "946702800", # unknown
|
||||||
"mux": "", # unknown
|
"mux": "", # unknown
|
||||||
@ -190,6 +198,10 @@ class MembershipWorks:
|
|||||||
)
|
)
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
raise MembershipWorksRemoteError("csv generation", r)
|
raise MembershipWorksRemoteError("csv generation", r)
|
||||||
|
|
||||||
|
if r.text[0] == "\ufeff":
|
||||||
|
r.encoding = r.encoding + "-sig"
|
||||||
|
|
||||||
return list(csv.DictReader(StringIO(r.text)))
|
return list(csv.DictReader(StringIO(r.text)))
|
||||||
|
|
||||||
def get_transactions(self, start_date, end_date, json=False):
|
def get_transactions(self, start_date, end_date, json=False):
|
||||||
@ -200,10 +212,10 @@ class MembershipWorks:
|
|||||||
json gets a different version of the transactions list,
|
json gets a different version of the transactions list,
|
||||||
which contains a different set information
|
which contains a different set information
|
||||||
"""
|
"""
|
||||||
r = self._get(
|
r = self._get_v1(
|
||||||
BASE_URL + "csv",
|
BASE_URL + "/v1/csv",
|
||||||
params={
|
params={
|
||||||
"crm": "12,13,14,18,19", # transaction types, see CRM
|
"crm": ",".join(str(k) for k in CRM.keys()),
|
||||||
**({"txl": ""} if json else {}),
|
**({"txl": ""} if json else {}),
|
||||||
"sdp": start_date.strftime("%s"),
|
"sdp": start_date.strftime("%s"),
|
||||||
"edp": end_date.strftime("%s"),
|
"edp": end_date.strftime("%s"),
|
||||||
@ -214,6 +226,9 @@ class MembershipWorks:
|
|||||||
if json:
|
if json:
|
||||||
return r.json()
|
return r.json()
|
||||||
else:
|
else:
|
||||||
|
if r.text[0] == "\ufeff":
|
||||||
|
r.encoding = r.encoding + "-sig"
|
||||||
|
|
||||||
return list(csv.DictReader(StringIO(r.text)))
|
return list(csv.DictReader(StringIO(r.text)))
|
||||||
|
|
||||||
def get_all_members(self):
|
def get_all_members(self):
|
||||||
@ -222,3 +237,29 @@ class MembershipWorks:
|
|||||||
fields = self._all_fields()
|
fields = self._all_fields()
|
||||||
members = self.get_members(folders, ",".join(fields.keys()))
|
members = self.get_members(folders, ",".join(fields.keys()))
|
||||||
return members
|
return members
|
||||||
|
|
||||||
|
def get_events_list(self, start_date: datetime.datetime):
|
||||||
|
"""Retrive a list of events since start_date"""
|
||||||
|
r = self.sess.get(
|
||||||
|
BASE_URL + "/v2/events",
|
||||||
|
params={
|
||||||
|
"sdp": start_date.strftime("%s"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def get_event_by_eid(self, eid: str):
|
||||||
|
"""Retrieve a specific event by its event id (eid)"""
|
||||||
|
r = self.sess.get(
|
||||||
|
BASE_URL + "/v2/event",
|
||||||
|
params={"eid": eid},
|
||||||
|
)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def get_event_by_url(self, url: str):
|
||||||
|
"""Retrieve a specific event by its url"""
|
||||||
|
r = self.sess.get(
|
||||||
|
BASE_URL + "/v2/event",
|
||||||
|
params={"url": url},
|
||||||
|
)
|
||||||
|
return r.json()
|
||||||
|
@ -22,7 +22,6 @@ class Config:
|
|||||||
self.DOOR_PASSWORD,
|
self.DOOR_PASSWORD,
|
||||||
name=doorName,
|
name=doorName,
|
||||||
access=doorData["access"],
|
access=doorData["access"],
|
||||||
cert=self.doorControllerCA_BUNDLE
|
|
||||||
)
|
)
|
||||||
for doorName, doorData in self.doorControllers.items()
|
for doorName, doorData in self.doorControllers.items()
|
||||||
}
|
}
|
||||||
|
@ -72,15 +72,9 @@ class MembershipworksMember(Member):
|
|||||||
else:
|
else:
|
||||||
self.credentials = set()
|
self.credentials = set()
|
||||||
|
|
||||||
self.onHold = data["Account on Hold"] != ""
|
self.onHold = (
|
||||||
self.limitedOperations = (
|
data["Account on Hold"] != ""
|
||||||
data[
|
or data["CMS Membership on hold"] == "CMS Membership on hold"
|
||||||
"Access Permitted Using Membership Level Schedule During COVID-19 Limited Operations"
|
|
||||||
]
|
|
||||||
== "Y"
|
|
||||||
)
|
|
||||||
self.staffedLimitedOperations = (
|
|
||||||
data["Access Permitted During COVID-19 Staffed Period Only"] == "Y"
|
|
||||||
)
|
)
|
||||||
self.formerMember = formerMember
|
self.formerMember = formerMember
|
||||||
|
|
||||||
@ -106,14 +100,8 @@ class MembershipworksMember(Member):
|
|||||||
|
|
||||||
schedules = []
|
schedules = []
|
||||||
if door.name in self.doorAccess and not self.onHold and not self.formerMember:
|
if door.name in self.doorAccess and not self.onHold and not self.formerMember:
|
||||||
# members should get their normal schedules
|
|
||||||
if self.limitedOperations or "CMS Staff" in self.levels:
|
|
||||||
schedules = self.schedules + doorLevels
|
schedules = self.schedules + doorLevels
|
||||||
|
|
||||||
# members should get only the staffed hours schedule
|
|
||||||
if self.staffedLimitedOperations:
|
|
||||||
schedules += ["Staffed Hours"]
|
|
||||||
|
|
||||||
dm = DoorMember(
|
dm = DoorMember(
|
||||||
door,
|
door,
|
||||||
forename=self.forename,
|
forename=self.forename,
|
||||||
@ -133,7 +121,6 @@ class MembershipworksMember(Member):
|
|||||||
return (
|
return (
|
||||||
super().__str__()
|
super().__str__()
|
||||||
+ f"""OnHold? {self.onHold}
|
+ f"""OnHold? {self.onHold}
|
||||||
Limited Operations Access? {self.limitedOperations}
|
|
||||||
Former Member? {self.formerMember}
|
Former Member? {self.formerMember}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@ -178,14 +165,48 @@ class DoorMember(Member):
|
|||||||
"custom2": self.membershipWorksID,
|
"custom2": self.membershipWorksID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def make_schedules(self, schedulesMap):
|
||||||
|
roles = [
|
||||||
|
E.Role(
|
||||||
|
{
|
||||||
|
"roleID": self.cardholderID,
|
||||||
|
"scheduleID": schedulesMap[schedule],
|
||||||
|
"resourceID": "0",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for schedule in self.schedules
|
||||||
|
]
|
||||||
|
|
||||||
|
return E.RoleSet(
|
||||||
|
{"action": "UD", "roleSetID": self.cardholderID}, E.Roles(*roles)
|
||||||
|
)
|
||||||
|
|
||||||
|
def make_credentials(self, newCredentials, cardFormats):
|
||||||
|
out = [
|
||||||
|
E.Credential(
|
||||||
|
{
|
||||||
|
"formatName": str(credential.code[0]),
|
||||||
|
"cardNumber": str(credential.code[1]),
|
||||||
|
"formatID": cardFormats[str(credential.code[0])],
|
||||||
|
"isCard": "true",
|
||||||
|
"cardholderID": self.cardholderID,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for credential in newCredentials
|
||||||
|
]
|
||||||
|
|
||||||
|
return E.Credentials({"action": "AD"}, *out)
|
||||||
|
|
||||||
|
|
||||||
def update_door(door, members):
|
def update_door(door, members):
|
||||||
|
cardFormats = door.get_cardFormats()
|
||||||
cardholders = {
|
cardholders = {
|
||||||
member.membershipWorksID: member
|
member.membershipWorksID: member
|
||||||
for member in [
|
for member in [
|
||||||
DoorMember.from_cardholder(ch, door) for ch in door.get_cardholders()
|
DoorMember.from_cardholder(ch, door) for ch in door.get_cardholders()
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
schedulesMap = door.get_scheduleMap()
|
||||||
allCredentials = set(
|
allCredentials = set(
|
||||||
Credential(hex=c.attrib["rawCardNumber"]) for c in door.get_credentials()
|
Credential(hex=c.attrib["rawCardNumber"]) for c in door.get_credentials()
|
||||||
)
|
)
|
||||||
@ -197,7 +218,9 @@ def update_door(door, members):
|
|||||||
if member.membershipWorksID not in cardholders:
|
if member.membershipWorksID not in cardholders:
|
||||||
print("- Adding Member {member.forename} {member.surname}:")
|
print("- Adding Member {member.forename} {member.surname}:")
|
||||||
print(f" - {member.attribs()}")
|
print(f" - {member.attribs()}")
|
||||||
resp = door.add_cardholder(member.attribs)
|
resp = door.doXMLRequest(
|
||||||
|
ROOT(E.Cardholders({"action": "AD"}, E.Cardholder(member.attribs())))
|
||||||
|
)
|
||||||
member.cardholderID = resp.find("{*}Cardholders/{*}Cardholder").attrib[
|
member.cardholderID = resp.find("{*}Cardholders/{*}Cardholder").attrib[
|
||||||
"cardholderID"
|
"cardholderID"
|
||||||
]
|
]
|
||||||
@ -216,7 +239,14 @@ def update_door(door, members):
|
|||||||
print(f"- Updating profile for {member.forename} {member.surname}")
|
print(f"- Updating profile for {member.forename} {member.surname}")
|
||||||
print(f" - Old: {ch.attribs()}")
|
print(f" - Old: {ch.attribs()}")
|
||||||
print(f" - New: {member.attribs()}")
|
print(f" - New: {member.attribs()}")
|
||||||
door.update_cardholder(member.cardholderID, member.attribs)
|
door.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
E.Cardholders(
|
||||||
|
{"action": "UD", "cardholderID": member.cardholderID},
|
||||||
|
E.CardHolder(member.attribs()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if member.credentials != ch.credentials:
|
if member.credentials != ch.credentials:
|
||||||
print(f"- Updating card for {member.forename} {member.surname}")
|
print(f"- Updating card for {member.forename} {member.surname}")
|
||||||
@ -234,7 +264,18 @@ def update_door(door, members):
|
|||||||
|
|
||||||
# cards removed, and won't be reassigned to someone else
|
# cards removed, and won't be reassigned to someone else
|
||||||
for card in (oldCards - newCards) - allNewCards:
|
for card in (oldCards - newCards) - allNewCards:
|
||||||
door.assign_credential(card, None)
|
door.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
E.Credentials(
|
||||||
|
{
|
||||||
|
"action": "UD",
|
||||||
|
"rawCardNumber": card.hex,
|
||||||
|
"isCard": "true",
|
||||||
|
},
|
||||||
|
E.Credential({"cardholderID": ""}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if newCards - oldCards: # cards added
|
if newCards - oldCards: # cards added
|
||||||
for card in newCards & allNewCards: # new card exists in another member
|
for card in newCards & allNewCards: # new card exists in another member
|
||||||
@ -250,11 +291,28 @@ def update_door(door, members):
|
|||||||
|
|
||||||
# card existed in door, and needs to be reassigned
|
# card existed in door, and needs to be reassigned
|
||||||
for card in newCards & allCredentials:
|
for card in newCards & allCredentials:
|
||||||
door.assign_credential(card, member.cardholderID)
|
door.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
E.Credentials(
|
||||||
|
{
|
||||||
|
"action": "UD",
|
||||||
|
"rawCardNumber": card.hex,
|
||||||
|
"isCard": "true",
|
||||||
|
},
|
||||||
|
E.Credential({"cardholderID": member.cardholderID}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# cards that never existed, and need to be created
|
# cards that never existed, and need to be created
|
||||||
if newCards - allCredentials:
|
if newCards - allCredentials:
|
||||||
door.add_credentials(newCards - allCredentials, member.cardholderID)
|
door.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
member.make_credentials(
|
||||||
|
newCards - allCredentials, cardFormats
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if member.schedules != ch.schedules:
|
if member.schedules != ch.schedules:
|
||||||
print(
|
print(
|
||||||
@ -262,7 +320,7 @@ def update_door(door, members):
|
|||||||
+ f" {member.forename} {member.surname}:"
|
+ f" {member.forename} {member.surname}:"
|
||||||
+ f" {ch.schedules} -> {member.schedules}"
|
+ f" {ch.schedules} -> {member.schedules}"
|
||||||
)
|
)
|
||||||
door.set_cardholder_schedules(member.cardholderID, member.schedules)
|
door.doXMLRequest(ROOT(member.make_schedules(schedulesMap)))
|
||||||
|
|
||||||
# TODO: delete cardholders that are no longer members?
|
# TODO: delete cardholders that are no longer members?
|
||||||
|
|
||||||
@ -271,7 +329,7 @@ def main():
|
|||||||
config = Config()
|
config = Config()
|
||||||
membershipworks = config.membershipworks
|
membershipworks = config.membershipworks
|
||||||
membershipworks_attributes = (
|
membershipworks_attributes = (
|
||||||
"_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf,xeh,xse,xlo,xxc"
|
"_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf,xeh,xse"
|
||||||
)
|
)
|
||||||
|
|
||||||
memberData = membershipworks.get_members(
|
memberData = membershipworks.get_members(
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import bitstring
|
import bitstring
|
||||||
from typing import Tuple, Optional
|
|
||||||
|
|
||||||
# Reference for H10301 card format:
|
# Reference for H10301 card format:
|
||||||
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
|
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
|
||||||
|
|
||||||
|
|
||||||
class Credential:
|
class Credential:
|
||||||
def __init__(
|
def __init__(self, code=None, hex=None):
|
||||||
self, code: Optional[Tuple[int, int]] = None, hex: Optional[str] = None
|
|
||||||
) -> None:
|
|
||||||
if code is None and hex is None:
|
if code is None and hex is None:
|
||||||
raise TypeError("Must set either code or hex for a Credential")
|
raise TypeError("Must set either code or hex for a Credential")
|
||||||
elif code is not None and hex is not None:
|
elif code is not None and hex is not None:
|
||||||
@ -24,24 +21,21 @@ class Credential:
|
|||||||
elif hex is not None:
|
elif hex is not None:
|
||||||
self.bits = bitstring.Bits(hex=hex)
|
self.bits = bitstring.Bits(hex=hex)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self):
|
||||||
return f"Credential({self.code})"
|
return f"Credential({self.code})"
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
def __eq__(self, other):
|
||||||
if isinstance(other, Credential):
|
|
||||||
return self.bits == other.bits
|
return self.bits == other.bits
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self):
|
||||||
return self.bits.int
|
return self.bits.int
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def code(self) -> Tuple[int, int]:
|
def code(self):
|
||||||
facility = self.bits[7:15].uint
|
facility = self.bits[7:15].uint
|
||||||
code = self.bits[15:31].uint
|
code = self.bits[15:31].uint
|
||||||
return (facility, code)
|
return (facility, code)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hex(self) -> str:
|
def hex(self):
|
||||||
return self.bits.hex.upper()
|
return self.bits.hex.upper()
|
||||||
|
@ -1,25 +1,11 @@
|
|||||||
import csv
|
import csv
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from typing import (
|
|
||||||
IO,
|
|
||||||
Callable,
|
|
||||||
Iterable,
|
|
||||||
List,
|
|
||||||
Literal,
|
|
||||||
Mapping,
|
|
||||||
Optional,
|
|
||||||
Tuple,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import urllib3
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from lxml.builder import ElementMaker
|
from lxml.builder import ElementMaker
|
||||||
from requests import Session
|
|
||||||
from requests.adapters import HTTPAdapter
|
|
||||||
|
|
||||||
from .Credential import Credential
|
|
||||||
|
|
||||||
E_plain = ElementMaker(nsmap={"hid": "http://www.hidglobal.com/VertX"})
|
E_plain = ElementMaker(nsmap={"hid": "http://www.hidglobal.com/VertX"})
|
||||||
E = ElementMaker(
|
E = ElementMaker(
|
||||||
@ -36,65 +22,33 @@ fieldnames = "CardNumber,CardFormat,PinRequired,PinCode,ExtendedAccess,ExpiryDat
|
|||||||
","
|
","
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: where should this live?
|
||||||
class HostNameIgnoringAdapter(HTTPAdapter):
|
# it's fine, ssl certs are for losers anyway
|
||||||
def init_poolmanager(self, *args, **kwargs) -> None:
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
super().init_poolmanager(*args, **kwargs, assert_hostname=False)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteError(Exception):
|
class RemoteError(Exception):
|
||||||
def __init__(self, r: requests.Response) -> None:
|
def __init__(self, r):
|
||||||
super().__init__(f"Door Updating Error: {r.status_code} {r.reason}\n{r.text}")
|
super().__init__(f"Door Updating Error: {r.status_code} {r.reason}\n{r.text}")
|
||||||
|
|
||||||
|
|
||||||
class DoorController:
|
class DoorController:
|
||||||
def __init__(
|
def __init__(self, ip, username, password, name="", access=""):
|
||||||
self,
|
|
||||||
ip: str,
|
|
||||||
username: str,
|
|
||||||
password: str,
|
|
||||||
name: str = "",
|
|
||||||
access: str = "",
|
|
||||||
cert: Optional[str] = None,
|
|
||||||
) -> None:
|
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.name = name
|
self.name = name
|
||||||
self.access = access
|
self.access = access
|
||||||
self.session = Session()
|
|
||||||
if cert is not None:
|
|
||||||
self.session.mount("https://", HostNameIgnoringAdapter())
|
|
||||||
self.session.verify = cert
|
|
||||||
|
|
||||||
self._cardFormats: Optional[Mapping[str, str]] = None
|
def doImport(self, params=None, files=None):
|
||||||
self._scheduleMap: Optional[Mapping[str, str]] = None
|
|
||||||
|
|
||||||
# lazy evaluated, hopefully won't change for the lifetime of this object
|
|
||||||
@property
|
|
||||||
def cardFormats(self) -> Mapping[str, str]:
|
|
||||||
if not self._cardFormats:
|
|
||||||
self._cardFormats = self.get_cardFormats()
|
|
||||||
return self._cardFormats
|
|
||||||
|
|
||||||
@property
|
|
||||||
def scheduleMap(self) -> Mapping[str, str]:
|
|
||||||
if not self._scheduleMap:
|
|
||||||
self._scheduleMap = self.get_scheduleMap()
|
|
||||||
return self._scheduleMap
|
|
||||||
|
|
||||||
def doImport(
|
|
||||||
self,
|
|
||||||
params: Optional[Mapping[str, str]] = None,
|
|
||||||
files: Optional[Mapping[str, Tuple[str, Union[IO[str], str], str]]] = None,
|
|
||||||
) -> None:
|
|
||||||
"""Send a request to the door control import script"""
|
"""Send a request to the door control import script"""
|
||||||
r = self.session.post(
|
r = requests.post(
|
||||||
"https://" + self.ip + "/cgi-bin/import.cgi",
|
"https://" + self.ip + "/cgi-bin/import.cgi",
|
||||||
params=params,
|
params=params,
|
||||||
files=files,
|
files=files,
|
||||||
auth=requests.auth.HTTPDigestAuth(self.username, self.password),
|
auth=requests.auth.HTTPDigestAuth(self.username, self.password),
|
||||||
timeout=60,
|
timeout=60,
|
||||||
|
verify=False,
|
||||||
) # ignore insecure SSL
|
) # ignore insecure SSL
|
||||||
xml = etree.XML(r.content)
|
xml = etree.XML(r.content)
|
||||||
if (
|
if (
|
||||||
@ -103,7 +57,7 @@ class DoorController:
|
|||||||
):
|
):
|
||||||
raise RemoteError(r)
|
raise RemoteError(r)
|
||||||
|
|
||||||
def doCSVImport(self, csv: Union[IO[str], str]) -> None:
|
def doCSVImport(self, csv):
|
||||||
"""Do the CSV import procedure on a door control"""
|
"""Do the CSV import procedure on a door control"""
|
||||||
self.doImport({"task": "importInit"})
|
self.doImport({"task": "importInit"})
|
||||||
self.doImport(
|
self.doImport(
|
||||||
@ -112,29 +66,22 @@ class DoorController:
|
|||||||
)
|
)
|
||||||
self.doImport({"task": "importDone"})
|
self.doImport({"task": "importDone"})
|
||||||
|
|
||||||
def doXMLRequest(
|
def doXMLRequest(self, xml, prefix=b'<?xml version="1.0" encoding="UTF-8"?>'):
|
||||||
self,
|
|
||||||
xml: Union[etree.Element, bytes],
|
|
||||||
prefix: bytes = b'<?xml version="1.0" encoding="UTF-8"?>',
|
|
||||||
) -> etree.XML:
|
|
||||||
if not isinstance(xml, bytes):
|
if not isinstance(xml, bytes):
|
||||||
xml = etree.tostring(xml)
|
xml = etree.tostring(xml)
|
||||||
r = self.session.get(
|
r = requests.get(
|
||||||
"https://" + self.ip + "/cgi-bin/vertx_xml.cgi",
|
"https://" + self.ip + "/cgi-bin/vertx_xml.cgi",
|
||||||
params={"XML": prefix + xml},
|
params={"XML": prefix + xml},
|
||||||
auth=requests.auth.HTTPDigestAuth(self.username, self.password),
|
auth=requests.auth.HTTPDigestAuth(self.username, self.password),
|
||||||
|
verify=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if r.status_code != 200:
|
|
||||||
raise RemoteError(r)
|
|
||||||
|
|
||||||
# probably meed to be more sane about this
|
|
||||||
resp_xml = etree.XML(r.content)
|
resp_xml = etree.XML(r.content)
|
||||||
if len(resp_xml.findall("{*}Error")) > 0:
|
# probably meed to be more sane about this
|
||||||
|
if r.status_code != 200 or len(resp_xml.findall("{*}Error")) > 0:
|
||||||
raise RemoteError(r)
|
raise RemoteError(r)
|
||||||
return resp_xml
|
return resp_xml
|
||||||
|
|
||||||
def get_scheduleMap(self) -> Mapping[str, str]:
|
def get_scheduleMap(self):
|
||||||
schedules = self.doXMLRequest(
|
schedules = self.doXMLRequest(
|
||||||
ROOT(E.Schedules({"action": "LR", "recordOffset": "0", "recordCount": "8"}))
|
ROOT(E.Schedules({"action": "LR", "recordOffset": "0", "recordCount": "8"}))
|
||||||
)
|
)
|
||||||
@ -142,7 +89,7 @@ class DoorController:
|
|||||||
fmt.attrib["scheduleName"]: fmt.attrib["scheduleID"] for fmt in schedules[0]
|
fmt.attrib["scheduleName"]: fmt.attrib["scheduleID"] for fmt in schedules[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_schedules(self) -> etree.Element:
|
def get_schedules(self):
|
||||||
# TODO: might be able to do in one request
|
# TODO: might be able to do in one request
|
||||||
schedules = self.doXMLRequest(ROOT(E.Schedules({"action": "LR"})))
|
schedules = self.doXMLRequest(ROOT(E.Schedules({"action": "LR"})))
|
||||||
etree.dump(schedules)
|
etree.dump(schedules)
|
||||||
@ -159,7 +106,7 @@ class DoorController:
|
|||||||
)
|
)
|
||||||
return ROOT(E_corp.Schedules({"action": "AD"}, *[s[0] for s in data]))
|
return ROOT(E_corp.Schedules({"action": "AD"}, *[s[0] for s in data]))
|
||||||
|
|
||||||
def set_schedules(self, schedules: etree.Element) -> None:
|
def set_schedules(self, schedules):
|
||||||
# clear all people
|
# clear all people
|
||||||
outString = StringIO()
|
outString = StringIO()
|
||||||
writer = csv.DictWriter(outString, fieldnames)
|
writer = csv.DictWriter(outString, fieldnames)
|
||||||
@ -184,27 +131,7 @@ class DoorController:
|
|||||||
# load new schedules
|
# load new schedules
|
||||||
self.doXMLRequest(schedules)
|
self.doXMLRequest(schedules)
|
||||||
|
|
||||||
def set_cardholder_schedules(
|
def get_cardFormats(self):
|
||||||
self, cardholderID: str, schedules: Iterable[str]
|
|
||||||
) -> etree.XML:
|
|
||||||
roles = [
|
|
||||||
E.Role(
|
|
||||||
{
|
|
||||||
"roleID": cardholderID,
|
|
||||||
"scheduleID": self.scheduleMap[schedule],
|
|
||||||
"resourceID": "0",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for schedule in schedules
|
|
||||||
]
|
|
||||||
|
|
||||||
roleSet = E.RoleSet(
|
|
||||||
{"action": "UD", "roleSetID": cardholderID}, E.Roles(*roles)
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.doXMLRequest(ROOT(roleSet))
|
|
||||||
|
|
||||||
def get_cardFormats(self) -> Mapping[str, str]:
|
|
||||||
cardFormats = self.doXMLRequest(
|
cardFormats = self.doXMLRequest(
|
||||||
ROOT(E.CardFormats({"action": "LR", "responseFormat": "expanded"}))
|
ROOT(E.CardFormats({"action": "LR", "responseFormat": "expanded"}))
|
||||||
)
|
)
|
||||||
@ -214,9 +141,7 @@ class DoorController:
|
|||||||
for fmt in cardFormats[0].findall("{*}CardFormat[{*}FixedField]")
|
for fmt in cardFormats[0].findall("{*}CardFormat[{*}FixedField]")
|
||||||
}
|
}
|
||||||
|
|
||||||
def set_cardFormat(
|
def set_cardFormat(self, formatName, templateID, facilityCode):
|
||||||
self, formatName: str, templateID: int, facilityCode: int
|
|
||||||
) -> etree.XML:
|
|
||||||
# TODO: add ability to delete formats
|
# TODO: add ability to delete formats
|
||||||
# delete example: <hid:CardFormats action="DD" formatID="7-1-244"/>
|
# delete example: <hid:CardFormats action="DD" formatID="7-1-244"/>
|
||||||
|
|
||||||
@ -231,14 +156,8 @@ class DoorController:
|
|||||||
)
|
)
|
||||||
return self.doXMLRequest(el)
|
return self.doXMLRequest(el)
|
||||||
|
|
||||||
def get_records(
|
def get_records(self, req, count, params={}, stopFunction=None):
|
||||||
self,
|
result = []
|
||||||
req: ElementMaker,
|
|
||||||
count: int,
|
|
||||||
params: Mapping[str, str] = {},
|
|
||||||
stopFunction: Optional[Callable[[List[etree.Element]], bool]] = None,
|
|
||||||
) -> List[etree.Element]:
|
|
||||||
result: List[etree.Element] = []
|
|
||||||
recordCount = 0
|
recordCount = 0
|
||||||
moreRecords = True
|
moreRecords = True
|
||||||
|
|
||||||
@ -270,76 +189,17 @@ class DoorController:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_cardholders(self) -> List[etree.Element]:
|
def get_cardholders(self):
|
||||||
return self.get_records(E.Cardholders, 1000, {"responseFormat": "expanded"})
|
return self.get_records(E.Cardholders, 1000, {"responseFormat": "expanded"})
|
||||||
|
|
||||||
def add_cardholder(self, attribs: Mapping[str, str]) -> etree.XML:
|
def get_credentials(self):
|
||||||
return self.doXMLRequest(
|
|
||||||
ROOT(E.Cardholders({"action": "AD"}, E.Cardholder(attribs)))
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_cardholder(
|
|
||||||
self, cardholderID: str, attribs: Mapping[str, str]
|
|
||||||
) -> etree.XML:
|
|
||||||
return self.doXMLRequest(
|
|
||||||
ROOT(
|
|
||||||
E.Cardholders(
|
|
||||||
{"action": "UD", "cardholderID": cardholderID},
|
|
||||||
E.CardHolder(attribs),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_credentials(self) -> List[etree.Element]:
|
|
||||||
return self.get_records(E.Credentials, 1000)
|
return self.get_records(E.Credentials, 1000)
|
||||||
|
|
||||||
def add_credentials(
|
def get_events(self, threshold):
|
||||||
self, credentials: Iterable[Credential], cardholderID: Optional[str] = None
|
def event_newer_than_threshold(event):
|
||||||
) -> etree.XML:
|
|
||||||
"""Create new Credentials. If a cardholderID is provided, assign the
|
|
||||||
new credentials to that cardholder"""
|
|
||||||
creds = [
|
|
||||||
E.Credential(
|
|
||||||
{
|
|
||||||
"formatName": str(credential.code[0]),
|
|
||||||
"cardNumber": str(credential.code[1]),
|
|
||||||
"formatID": self.cardFormats[str(credential.code[0])],
|
|
||||||
"isCard": "true",
|
|
||||||
**({"cardholderID": cardholderID} if cardholderID else {}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for credential in credentials
|
|
||||||
]
|
|
||||||
|
|
||||||
return self.doXMLRequest(ROOT(E.Credentials({"action": "AD"}, *creds)))
|
|
||||||
|
|
||||||
def assign_credential(
|
|
||||||
self, credential: Credential, cardholderID: Optional[str] = None
|
|
||||||
) -> etree.XML:
|
|
||||||
# empty string removes assignment
|
|
||||||
if cardholderID is None:
|
|
||||||
cardholderID = ""
|
|
||||||
|
|
||||||
return self.doXMLRequest(
|
|
||||||
ROOT(
|
|
||||||
E.Credentials(
|
|
||||||
{
|
|
||||||
"action": "UD",
|
|
||||||
"rawCardNumber": credential.hex,
|
|
||||||
"isCard": "true",
|
|
||||||
},
|
|
||||||
E.Credential({"cardholderID": cardholderID}),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_events(self, threshold: datetime) -> List[etree.Element]:
|
|
||||||
def event_newer_than_threshold(event: etree.Element) -> bool:
|
|
||||||
return datetime.fromisoformat(event.attrib["timestamp"]) > threshold
|
return datetime.fromisoformat(event.attrib["timestamp"]) > threshold
|
||||||
|
|
||||||
def last_event_newer_than_threshold(
|
def last_event_newer_than_threshold(events):
|
||||||
events: List[etree.Element],
|
|
||||||
) -> etree.Element:
|
|
||||||
return (not events) or event_newer_than_threshold(events[-1])
|
return (not events) or event_newer_than_threshold(events[-1])
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -350,13 +210,13 @@ class DoorController:
|
|||||||
if event_newer_than_threshold(event)
|
if event_newer_than_threshold(event)
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_lock(self) -> Union[Literal["locked"], Literal["unlocked"]]:
|
def get_lock(self):
|
||||||
el = ROOT(E.Doors({"action": "LR", "responseFormat": "status"}))
|
el = ROOT(E.Doors({"action": "LR", "responseFormat": "status"}))
|
||||||
xml = self.doXMLRequest(el)
|
xml = self.doXMLRequest(el)
|
||||||
relayState = xml.find("./{*}Doors/{*}Door").attrib["relayState"]
|
relayState = xml.find("./{*}Doors/{*}Door").attrib["relayState"]
|
||||||
return "unlocked" if relayState == "set" else "locked"
|
return "unlocked" if relayState == "set" else "locked"
|
||||||
|
|
||||||
def set_lock(self, lock: bool = True) -> etree.XML:
|
def set_lock(self, lock=True):
|
||||||
el = ROOT(
|
el = ROOT(
|
||||||
E.Doors({"action": "CM", "command": "lockDoor" if lock else "unlockDoor"})
|
E.Doors({"action": "CM", "command": "lockDoor" if lock else "unlockDoor"})
|
||||||
)
|
)
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from ..Credential import Credential
|
|
||||||
|
|
||||||
|
|
||||||
def test_code_to_hex() -> None:
|
|
||||||
cred = Credential(code=(123, 45678))
|
|
||||||
assert cred.hex == "02F764DD"
|
|
||||||
|
|
||||||
|
|
||||||
def test_hex_to_code() -> None:
|
|
||||||
cred = Credential(hex="02F764DD")
|
|
||||||
assert cred.code == (123, 45678)
|
|
@ -97,7 +97,11 @@ def main():
|
|||||||
config = Config()
|
config = Config()
|
||||||
database.init(
|
database.init(
|
||||||
**config.HID_DB,
|
**config.HID_DB,
|
||||||
**{"charset": "utf8", "sql_mode": "PIPES_AS_CONCAT", "use_unicode": True,}
|
**{
|
||||||
|
"charset": "utf8",
|
||||||
|
"sql_mode": "PIPES_AS_CONCAT",
|
||||||
|
"use_unicode": True,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
HIDEvent.create_table()
|
HIDEvent.create_table()
|
||||||
for door in config.doors.values():
|
for door in config.doors.values():
|
||||||
|
@ -22,6 +22,12 @@ def do_import(config):
|
|||||||
]
|
]
|
||||||
).on_conflict(action="update", preserve=[Flag.name, Flag.type]).execute()
|
).on_conflict(action="update", preserve=[Flag.name, Flag.type]).execute()
|
||||||
|
|
||||||
|
print("Getting folder membership...")
|
||||||
|
folders = {
|
||||||
|
folder_id: membershipworks.get_member_ids([folder_name])
|
||||||
|
for folder_name, folder_id in membershipworks._parse_flags()["folders"].items()
|
||||||
|
}
|
||||||
|
|
||||||
print("Getting/Updating members...")
|
print("Getting/Updating members...")
|
||||||
members = membershipworks.get_all_members()
|
members = membershipworks.get_all_members()
|
||||||
for m in members:
|
for m in members:
|
||||||
@ -41,10 +47,11 @@ def do_import(config):
|
|||||||
|
|
||||||
# update member's flags
|
# update member's flags
|
||||||
for type, flags in membershipworks._parse_flags().items():
|
for type, flags in membershipworks._parse_flags().items():
|
||||||
if type != "folders": # currently no way to retrieve this info
|
|
||||||
for flag, id in flags.items():
|
for flag, id in flags.items():
|
||||||
ml = MemberFlag(uid=member["Account ID"], flag_id=id)
|
ml = MemberFlag(uid=member["Account ID"], flag_id=id)
|
||||||
if member[flag]:
|
if (type == "folders" and member["Account ID"] in folders[id]) or (
|
||||||
|
type != "folders" and member[flag]
|
||||||
|
):
|
||||||
ml.magic_save()
|
ml.magic_save()
|
||||||
else:
|
else:
|
||||||
ml.delete_instance()
|
ml.delete_instance()
|
||||||
@ -75,7 +82,11 @@ def main():
|
|||||||
config = Config()
|
config = Config()
|
||||||
database.init(
|
database.init(
|
||||||
**config.MEMBERSHIPWORKS_DB,
|
**config.MEMBERSHIPWORKS_DB,
|
||||||
**{"charset": "utf8", "sql_mode": "PIPES_AS_CONCAT", "use_unicode": True,}
|
**{
|
||||||
|
"charset": "utf8",
|
||||||
|
"sql_mode": "PIPES_AS_CONCAT",
|
||||||
|
"use_unicode": True,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
do_import(config)
|
do_import(config)
|
||||||
|
@ -1,688 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json-schema.org/schema#",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"nam": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"adr": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"zip": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"cit": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"sta": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"con": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"ad1": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"loc": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"ad1",
|
|
||||||
"cit",
|
|
||||||
"con",
|
|
||||||
"loc",
|
|
||||||
"sta",
|
|
||||||
"zip"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"org": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"cur": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"ctc": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"atg": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"uid": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"typ": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"eml": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"phn": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"tpl": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"anm": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"lbl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"box": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"dat": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"typ": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"plh": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"req": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"rxp": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"err": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"lbl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"dtl": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"typ"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ttl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"dtl": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"dat"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dir": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"box",
|
|
||||||
"lbl"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"acc": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"lbl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"box": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"dat": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"typ": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"plh": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"req": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"rxp": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"err": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"lbl": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"_id",
|
|
||||||
"lbl",
|
|
||||||
"typ"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ttl": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"dat"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"box",
|
|
||||||
"lbl"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"adm": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"lbl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"box": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"ttl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"dat": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"typ": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"plh": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"req": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"rxp": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"err": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"lbl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"def": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"_id",
|
|
||||||
"typ"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dtl": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"dat"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"box",
|
|
||||||
"lbl"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dir": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"lbl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"box": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"htm": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"dat": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"box",
|
|
||||||
"lbl"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"crd": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"typ": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"dtl": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"dtl",
|
|
||||||
"typ"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ylp": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"dat": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"ttl": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"dat"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"evt": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"typ": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"req": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"lbl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"rxp": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"err": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"_id",
|
|
||||||
"lbl",
|
|
||||||
"req",
|
|
||||||
"typ"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"acc",
|
|
||||||
"adm",
|
|
||||||
"anm",
|
|
||||||
"crd",
|
|
||||||
"dir",
|
|
||||||
"evt",
|
|
||||||
"ylp"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"evg": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"ttl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"cal": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"opt": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"cal",
|
|
||||||
"opt",
|
|
||||||
"ttl"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dcc": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"lbl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"amt": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"typ": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"cnt": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"cap": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"_id",
|
|
||||||
"amt",
|
|
||||||
"lbl",
|
|
||||||
"typ"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dek": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"did": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"lbl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"dir": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"aon": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"typ": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"dek": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"pub": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"dtl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"mux": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"itv": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"lvl": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"ctc": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tnm": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"dnm": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"cur": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"_rt": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"bil": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"bid": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"ttl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"way": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"amt": {
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
"itv": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"mux": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"pub": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"dcc": {
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"dso": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"amt",
|
|
||||||
"bid",
|
|
||||||
"itv",
|
|
||||||
"mux",
|
|
||||||
"ttl",
|
|
||||||
"way"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cat": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"upg": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nyn": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"prp": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"psm": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"exp": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"enr": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"day": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"ttl": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"dtl": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"day",
|
|
||||||
"dtl",
|
|
||||||
"ttl"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"wen": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"pur": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"puk": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"puu": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pus": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pup": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"puq": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"_id",
|
|
||||||
"dek",
|
|
||||||
"did",
|
|
||||||
"dir",
|
|
||||||
"lbl",
|
|
||||||
"pub",
|
|
||||||
"typ"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SF": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"_fd": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"_ts": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"_re": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"SF",
|
|
||||||
"_fd",
|
|
||||||
"_ts",
|
|
||||||
"adr",
|
|
||||||
"atg",
|
|
||||||
"cur",
|
|
||||||
"dcc",
|
|
||||||
"dek",
|
|
||||||
"eml",
|
|
||||||
"end",
|
|
||||||
"evg",
|
|
||||||
"nam",
|
|
||||||
"org",
|
|
||||||
"tpl",
|
|
||||||
"typ",
|
|
||||||
"uid"
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,179 +0,0 @@
|
|||||||
import datetime
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import responses
|
|
||||||
|
|
||||||
from ..MembershipWorks import BASE_URL, MembershipWorks, MembershipWorksRemoteError
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_login():
|
|
||||||
membershipworks = MembershipWorks()
|
|
||||||
|
|
||||||
responses.add(
|
|
||||||
responses.POST, BASE_URL + "usr", json={"SF": "test1", "org": 10000}, status=200
|
|
||||||
)
|
|
||||||
|
|
||||||
membershipworks.login("test1@example.com", "test2")
|
|
||||||
|
|
||||||
assert membershipworks.auth_token == "test1"
|
|
||||||
assert membershipworks.org_num == 10000
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_login_fail():
|
|
||||||
membershipworks = MembershipWorks()
|
|
||||||
|
|
||||||
responses.add(
|
|
||||||
responses.POST, BASE_URL + "usr", json={"error": "Login/password not accepted"},
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(MembershipWorksRemoteError) as e:
|
|
||||||
membershipworks.login("test1@example.com", "test2")
|
|
||||||
|
|
||||||
assert (
|
|
||||||
e.exception.args[0]
|
|
||||||
== "Error when attempting login: 200 OK\n"
|
|
||||||
+ '{"error":"Login/password not accepted"}'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def membershipworks():
|
|
||||||
mw = MembershipWorks()
|
|
||||||
|
|
||||||
with responses.RequestsMock() as rsps:
|
|
||||||
rsps.add(
|
|
||||||
responses.POST,
|
|
||||||
BASE_URL + "usr",
|
|
||||||
json={
|
|
||||||
"SF": "test1",
|
|
||||||
"org": 10000,
|
|
||||||
"dek": [{"_id": "12345", "lbl": "test", "dek": 1}],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
mw.login("test1@example.com", "test2")
|
|
||||||
|
|
||||||
return mw
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_get_member_ids(membershipworks: MembershipWorks):
|
|
||||||
responses.add(
|
|
||||||
responses.GET,
|
|
||||||
BASE_URL + "ylp",
|
|
||||||
json={
|
|
||||||
"typ": "a",
|
|
||||||
"usr": [{"uid": "asdf", "nam": "test", "rnk": 12.345}],
|
|
||||||
"_re": 631,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ids = membershipworks.get_member_ids(["test"])
|
|
||||||
assert ids == ["asdf"]
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
@patch("memberPlumbing.MembershipWorks.MembershipWorks.get_member_ids")
|
|
||||||
def test_get_members(mockIDs, membershipworks: MembershipWorks):
|
|
||||||
mockIDs.return_value = ["asdf"]
|
|
||||||
|
|
||||||
responses.add(
|
|
||||||
responses.POST,
|
|
||||||
BASE_URL + "csv",
|
|
||||||
body=""""Account Name","First Name","Last Name","Account ID","Parent Account ID"
|
|
||||||
"bob whatever", "bob", "whatever", "aaaaaaaaaaaaaaaaaaaaaaaa", ""
|
|
||||||
"george whatever", "george", "whatever", "bbbbbbbbbbbbbbbbbbbbbbbb", ""
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
members = membershipworks.get_members(["test"], "_id,nam")
|
|
||||||
|
|
||||||
assert members == [
|
|
||||||
{
|
|
||||||
"Account Name": "bob whatever",
|
|
||||||
"First Name": ' "bob"',
|
|
||||||
"Last Name": ' "whatever"',
|
|
||||||
"Account ID": ' "aaaaaaaaaaaaaaaaaaaaaaaa"',
|
|
||||||
"Parent Account ID": ' ""',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Account Name": "george whatever",
|
|
||||||
"First Name": ' "george"',
|
|
||||||
"Last Name": ' "whatever"',
|
|
||||||
"Account ID": ' "bbbbbbbbbbbbbbbbbbbbbbbb"',
|
|
||||||
"Parent Account ID": ' ""',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_get_transactions_csv(membershipworks: MembershipWorks):
|
|
||||||
responses.add(
|
|
||||||
responses.GET,
|
|
||||||
BASE_URL + "csv",
|
|
||||||
body=""""Date","Name","Contact Person","Full Address","Street","City","State/Province","Postal Code","Country","Phone","Email","Membership Sub-Total","Event Sub-Total","Donation Sub-Total","Cart Sub-Total","Other Sub-Total","Handling","Total Tax","Transaction Total","Transaction Fee","Total Payment Due","Transaction Type","For","Items","Payment ID","Account ID","Discount Code","Note"
|
|
||||||
"Jan 01, 2020","whatever","","PO Box 0, fakeplace NH, US","PO Box 0","fakeplace","NH","","US","000-000-0000","example@example.com",1,0,0,0,0,0,0,1,0.32,0,"Membership","","CMS Membership on hold - Membership on hold","ch_aaaaaaaaaaaaaaaaaaaaaaaa","aaaaaaaaaaaaaaaaaaaaaaaa","",""
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
transactions = membershipworks.get_transactions(
|
|
||||||
datetime.datetime(2000, 1, 1), datetime.datetime(2021, 1, 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert transactions == [
|
|
||||||
{
|
|
||||||
"Date": "Jan 01, 2020",
|
|
||||||
"Name": "whatever",
|
|
||||||
"Contact Person": "",
|
|
||||||
"Full Address": "PO Box 0, fakeplace NH, US",
|
|
||||||
"Street": "PO Box 0",
|
|
||||||
"City": "fakeplace",
|
|
||||||
"State/Province": "NH",
|
|
||||||
"Postal Code": "",
|
|
||||||
"Country": "US",
|
|
||||||
"Phone": "000-000-0000",
|
|
||||||
"Email": "example@example.com",
|
|
||||||
"Membership Sub-Total": "1",
|
|
||||||
"Event Sub-Total": "0",
|
|
||||||
"Donation Sub-Total": "0",
|
|
||||||
"Cart Sub-Total": "0",
|
|
||||||
"Other Sub-Total": "0",
|
|
||||||
"Handling": "0",
|
|
||||||
"Total Tax": "0",
|
|
||||||
"Transaction Total": "1",
|
|
||||||
"Transaction Fee": "0.32",
|
|
||||||
"Total Payment Due": "0",
|
|
||||||
"Transaction Type": "Membership",
|
|
||||||
"For": "",
|
|
||||||
"Items": "CMS Membership on hold - Membership on hold",
|
|
||||||
"Payment ID": "ch_aaaaaaaaaaaaaaaaaaaaaaaa",
|
|
||||||
"Account ID": "aaaaaaaaaaaaaaaaaaaaaaaa",
|
|
||||||
"Discount Code": "",
|
|
||||||
"Note": "",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@responses.activate
|
|
||||||
def test_get_transactions_json(membershipworks: MembershipWorks):
|
|
||||||
data_json = [
|
|
||||||
{
|
|
||||||
"cur": "usd",
|
|
||||||
"sid": "ch_aaaaaaaaaaaaaaaaaaaaaaaa",
|
|
||||||
"typ": 12,
|
|
||||||
"_dp": 1585100000,
|
|
||||||
"sum": 1,
|
|
||||||
"fee": 0.32,
|
|
||||||
"uid": "aaaaaaaaaaaaaaaaaaaaaaaa",
|
|
||||||
"nam": "whatever",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
responses.add(responses.GET, BASE_URL + "csv", json=data_json)
|
|
||||||
|
|
||||||
transactions = membershipworks.get_transactions(
|
|
||||||
datetime.datetime(2000, 1, 1), datetime.datetime(2021, 1, 1), json=True
|
|
||||||
)
|
|
||||||
assert "&txl=" in responses.calls[0].request.url # json flag
|
|
||||||
assert transactions == data_json
|
|
@ -1,31 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
import jsonschema
|
|
||||||
import requests
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from ..common import MEMBERSHIPWORKS_PASSWORD, MEMBERSHIPWORKS_USERNAME
|
|
||||||
from ..MembershipWorks import BASE_URL
|
|
||||||
|
|
||||||
schemas_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "json-schemas")
|
|
||||||
|
|
||||||
|
|
||||||
def test_login():
|
|
||||||
r = requests.post(
|
|
||||||
BASE_URL + "usr",
|
|
||||||
data={
|
|
||||||
"_st": "all",
|
|
||||||
"eml": MEMBERSHIPWORKS_USERNAME,
|
|
||||||
"org": "10000",
|
|
||||||
"pwd": MEMBERSHIPWORKS_PASSWORD,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(os.path.join(schemas_dir, "usr.schema")) as f:
|
|
||||||
schema = json.load(f)
|
|
||||||
|
|
||||||
usr = r.json()
|
|
||||||
|
|
||||||
jsonschema.validate(usr, schema)
|
|
@ -5,13 +5,15 @@ import re
|
|||||||
import string
|
import string
|
||||||
|
|
||||||
from udm_rest_client.udm import UDM
|
from udm_rest_client.udm import UDM
|
||||||
from udm_rest_client.exceptions import NoObject
|
from udm_rest_client.exceptions import NoObject, UdmError
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
USER_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
USER_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||||
GROUP_BASE = "cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
GROUP_BASE = "cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||||
GROUPS_REGEX = "|".join(["Certified: .*", "Access .*\?", "CMS .*", "Volunteer: .*"])
|
GROUPS_REGEX = "|".join(
|
||||||
|
["Certified: .*", "Access .*\?", "CMS .*", "Volunteer: .*", "Database .*"]
|
||||||
|
)
|
||||||
RAND_PW_LEN = 20
|
RAND_PW_LEN = 20
|
||||||
|
|
||||||
|
|
||||||
@ -27,7 +29,7 @@ def sanitize_group_name(name):
|
|||||||
|
|
||||||
# From an API error message: "Username must only contain numbers, letters and dots!"
|
# From an API error message: "Username must only contain numbers, letters and dots!"
|
||||||
def sanitize_user_name(name):
|
def sanitize_user_name(name):
|
||||||
return re.sub(r"[^0-9a-z.]", ".", name.lower())
|
return re.sub(r"[^0-9a-z.]", ".", name.lower()).strip(".")
|
||||||
|
|
||||||
|
|
||||||
async def make_groups(group_mod, members):
|
async def make_groups(group_mod, members):
|
||||||
@ -52,7 +54,6 @@ async def _main():
|
|||||||
["Members", "CMS Staff"], "lvl,phn,eml,lbl,nam,end,_id"
|
["Members", "CMS Staff"], "lvl,phn,eml,lbl,nam,end,_id"
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: security!
|
|
||||||
async with UDM(**config.UCS) as udm:
|
async with UDM(**config.UCS) as udm:
|
||||||
user_mod = udm.get("users/user")
|
user_mod = udm.get("users/user")
|
||||||
group_mod = udm.get("groups/group")
|
group_mod = udm.get("groups/group")
|
||||||
@ -65,7 +66,7 @@ async def _main():
|
|||||||
try: # try to get an existing user to update
|
try: # try to get an existing user to update
|
||||||
user = await user_mod.get(f"uid={username},{USER_BASE}")
|
user = await user_mod.get(f"uid={username},{USER_BASE}")
|
||||||
except NoObject: # create a new user
|
except NoObject: # create a new user
|
||||||
# TODO: search by employeeID and rename users when needed
|
# TODO: search by employeeNumber and rename users when needed
|
||||||
user = await user_mod.new()
|
user = await user_mod.new()
|
||||||
|
|
||||||
# set a random password and ensure it is changed at next login
|
# set a random password and ensure it is changed at next login
|
||||||
@ -109,10 +110,17 @@ async def _main():
|
|||||||
if re.match(GROUPS_REGEX, group) is not None and value != ""
|
if re.match(GROUPS_REGEX, group) is not None and value != ""
|
||||||
]
|
]
|
||||||
# groups not from this script
|
# groups not from this script
|
||||||
other_old_groups = [g for g in user.props.groups if g[3:].startswith("MW_")]
|
other_old_groups = [
|
||||||
|
g for g in user.props.groups if not g[3:].startswith("MW_")
|
||||||
|
]
|
||||||
user.props.groups = other_old_groups + new_groups
|
user.props.groups = other_old_groups + new_groups
|
||||||
|
|
||||||
|
try:
|
||||||
await user.save()
|
await user.save()
|
||||||
|
except UdmError:
|
||||||
|
print("Failed to save user", username)
|
||||||
|
print(user.props)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
76
memberPlumbing/upcomingEvents.py
Normal file
76
memberPlumbing/upcomingEvents.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .MembershipWorks import MembershipWorks
|
||||||
|
|
||||||
|
|
||||||
|
def format_event(membershipworks: MembershipWorks, event):
|
||||||
|
event_details = membershipworks.get_event_by_eid(event["eid"])
|
||||||
|
url = (
|
||||||
|
"https://claremontmakerspace.org/events/#!event/register/"
|
||||||
|
+ event_details["url"]
|
||||||
|
)
|
||||||
|
if "lgo" in event_details:
|
||||||
|
img = f"""<img class="aligncenter" width="500" height="500" src="{event_details['lgo']['l']}">"""
|
||||||
|
else:
|
||||||
|
img = ""
|
||||||
|
# print(json.dumps(event_details))
|
||||||
|
return f"""<h2 style="text-align: center;">
|
||||||
|
<a href="{url}">
|
||||||
|
{img}
|
||||||
|
{event_details['ttl']}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<div>{event_details['szp']} — {event_details['ezp']}</div>
|
||||||
|
<div>
|
||||||
|
{event_details['dtl']}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{url}">Register for this class now!</a>"""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
membershipworks = config.membershipworks
|
||||||
|
events = membershipworks.get_events_list(datetime.now())
|
||||||
|
if "error" in events:
|
||||||
|
print("Error:", events["error"])
|
||||||
|
return
|
||||||
|
|
||||||
|
events_list = "\n<hr />\n\n".join(
|
||||||
|
format_event(membershipworks, event)
|
||||||
|
for event in events["evt"]
|
||||||
|
if event["ttl"] != "[TEMPLATE FOR COPYING]"
|
||||||
|
)
|
||||||
|
header = """<p><img class="aligncenter size-medium wp-image-2319" src="https://claremontmakerspace.org/wp-content/uploads/2019/03/CMS-Logo-b-y-g-300x168.png" alt="" width="300" height="168" /></a></p>
|
||||||
|
<p>Greetings Upper Valley Makers:</p>
|
||||||
|
<p>We have an exciting list of upcoming classes at the Claremont MakerSpace that we think might interest you.</p>
|
||||||
|
<strong>For most classes and events, CMS MEMBERSHIP IS NOT REQUIRED.</strong> That said, members receive a discount on registration and there are some classes/events that are for members only (this will be clearly noted in the event description).
|
||||||
|
|
||||||
|
<strong>Class policies</strong> (liability waiver, withdrawal, cancellation, etc.) can be found <a href="https://claremontmakerspace.org/class-policies/" data-wpel-link="internal">here</a>.
|
||||||
|
|
||||||
|
<strong>Please note: </strong>The Claremont MakerSpace currently requires masks for all visitors and members.
|
||||||
|
|
||||||
|
<strong>Instructors:</strong> Interested in teaching a class at CMS? Please fill out our <a href="https://docs.google.com/forms/d/e/1FAIpQLSdJyEVRJxzIczG784VkOm_DsNyv-VXRXYzlis8qlMdEOvHGpQ/viewform" target="_blank" rel="noreferrer noopener external" data-wpel-link="external">Class Proposal Form</a><a href="https://docs.google.com/forms/d/e/1FAIpQLSdJyEVRJxzIczG784VkOm_DsNyv-VXRXYzlis8qlMdEOvHGpQ/viewform" target="_blank" rel="noreferrer noopener external" data-wpel-link="external">.</a>
|
||||||
|
|
||||||
|
<strong>Tours:</strong> Want to see what the Claremont MakerSpace is all about? Tours are by appointment only due to COVID-19 restrictions. <a href="https://tickets.claremontmakerspace.org/open.php" target="_blank" rel="noreferrer noopener external" data-wpel-link="external">Contact Us</a> to schedule your tour where you can learn about all the awesome tools that the CMS offers access to, as well as how membership, classes, and studio spaces work.
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
"""
|
||||||
|
|
||||||
|
footer = """
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>Happy Makin’!</div>
|
||||||
|
<div>We are grateful for all of the public support that our 501(c)(3), non-profit organization receives. If you’d like to make a donation,please visit the <a href="https://claremontmakerspace.org/support/"><strong>Support Us page</strong></a> of our website.</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(header, events_list, footer)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
1437
poetry.lock
generated
1437
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -17,39 +17,23 @@ authors = ["Adam Goldsmith <adam@adamgoldsmith.name>"]
|
|||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.7"
|
python = "^3.7"
|
||||||
requests = "^2.23.0"
|
requests = "^2.23.0"
|
||||||
"ruamel.yaml" = "^0.16.10"
|
"ruamel.yaml" = "^0.17.20"
|
||||||
bitstring = "^3.1.6"
|
bitstring = "^3.1.6"
|
||||||
lxml = "^4.5.0"
|
lxml = "^4.5.0"
|
||||||
peewee = "^3.13.2"
|
peewee = "^3.13.2"
|
||||||
mysqlclient = "^1.4.6"
|
mysqlclient = "^2.1.0"
|
||||||
udm-rest-client = "^0.4.0"
|
udm-rest-client = "^1.0.6"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
black = "^19.10b0"
|
black = "^22.3.0"
|
||||||
isort = "^4.3.21"
|
isort = "^5.10.1"
|
||||||
taskipy = "^1.2.1"
|
|
||||||
pytest = "^5.4.2"
|
|
||||||
coverage = {extras = ["toml"], version = "^5.1"}
|
|
||||||
|
|
||||||
[tool.taskipy.tasks]
|
|
||||||
test = "pytest"
|
|
||||||
mypy = "mypy --strict memberPlumbing/hid/ stubs"
|
|
||||||
cov = "coverage run && coverage html"
|
|
||||||
|
|
||||||
[tool.coverage.run]
|
|
||||||
branch = true
|
|
||||||
source = ["memberPlumbing"]
|
|
||||||
omit = ["*/tests/*"]
|
|
||||||
command_line = "-m pytest"
|
|
||||||
|
|
||||||
[tool.coverage.report]
|
|
||||||
skip_empty = true
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
doorUpdater = 'memberPlumbing.doorUpdater:main'
|
doorUpdater = 'memberPlumbing.doorUpdater:main'
|
||||||
hidEvents = 'memberPlumbing.hidEvents:main'
|
hidEvents = 'memberPlumbing.hidEvents:main'
|
||||||
sqlExport = 'memberPlumbing.sqlExport:main'
|
sqlExport = 'memberPlumbing.sqlExport:main'
|
||||||
ucsAccounts = 'memberPlumbing.ucsAccounts:main'
|
ucsAccounts = 'memberPlumbing.ucsAccounts:main'
|
||||||
|
upcomingEvents = 'memberPlumbing.upcomingEvents:main'
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry>=0.12"]
|
requires = ["poetry>=0.12"]
|
||||||
|
10
systemd/ucsAccounts.service
Normal file
10
systemd/ucsAccounts.service
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Update UCS Accounts
|
||||||
|
OnFailure=status-email-admin@%n.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=adam
|
||||||
|
Type=oneshot
|
||||||
|
TimeoutStartSec=600
|
||||||
|
WorkingDirectory=/home/adam/memberPlumbing/
|
||||||
|
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run ucsAccounts
|
9
systemd/ucsAccounts.timer
Normal file
9
systemd/ucsAccounts.timer
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Hourly UCS Accounts update
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*:0/15
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
Loading…
Reference in New Issue
Block a user