forked from CMS/memberPlumbing
Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
e3176cd155 | |||
d3266a7d7f | |||
79678a8920 | |||
8b845bab05 | |||
ef23433c8f | |||
936effe5c7 | |||
e71fd48975 | |||
c0e43dd48e | |||
5478518d51 | |||
9cf12b1bdd | |||
981cb12aa6 | |||
0a5c80e87c | |||
|
aa92b77150 | ||
5e2fd5d427 | |||
f4f813c98f | |||
001a190947 | |||
|
2732c788c4 | ||
|
a1330ae637 | ||
66853e1156 | |||
363be0ba8c | |||
d8b3958c87 | |||
68b4b10c51 | |||
f85c26a844 | |||
2570aa3620 | |||
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,46 +151,44 @@ 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
|
||||||
def get_members(self, folders, columns):
|
def get_members(self, folders, columns):
|
||||||
""" Pull the members csv from the membershipworks api
|
"""Pull the members csv from the membershipworks api
|
||||||
folders: a list of the names of the folders to get
|
folders: a list of the names of the folders to get
|
||||||
(see folder_map in this function for mapping to ids)
|
(see folder_map in this function for mapping to ids)
|
||||||
columns: which columns to get"""
|
columns: which columns to get"""
|
||||||
ids = self.get_member_ids(folders)
|
ids = self.get_member_ids(folders)
|
||||||
|
|
||||||
# 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,20 +198,24 @@ 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):
|
||||||
"""Get the transactions between start_date and end_date
|
"""Get the transactions between start_date and end_date
|
||||||
|
|
||||||
Dates can be datetime.date or datetime.datetime
|
Dates can be datetime.date or datetime.datetime
|
||||||
|
|
||||||
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,13 +100,7 @@ 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
|
schedules = self.schedules + doorLevels
|
||||||
if self.limitedOperations or "CMS Staff" in self.levels:
|
|
||||||
schedules = self.schedules + doorLevels
|
|
||||||
|
|
||||||
# members should get only the staffed hours schedule
|
|
||||||
if self.staffedLimitedOperations:
|
|
||||||
schedules += ["Staffed Hours"]
|
|
||||||
|
|
||||||
dm = DoorMember(
|
dm = DoorMember(
|
||||||
door,
|
door,
|
||||||
@ -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():
|
||||||
|
@ -75,6 +75,7 @@ class Member(BaseModel):
|
|||||||
last_name = TextField(column_name="Last Name", null=True)
|
last_name = TextField(column_name="Last Name", null=True)
|
||||||
phone = TextField(column_name="Phone", null=True)
|
phone = TextField(column_name="Phone", null=True)
|
||||||
email = TextField(column_name="Email", null=True)
|
email = TextField(column_name="Email", null=True)
|
||||||
|
volunteer_email = TextField(column_name="Volunteer Email", null=True)
|
||||||
address_street = TextField(column_name="Address (Street)", null=True)
|
address_street = TextField(column_name="Address (Street)", null=True)
|
||||||
address_city = TextField(column_name="Address (City)", null=True)
|
address_city = TextField(column_name="Address (City)", null=True)
|
||||||
address_state_province = TextField(
|
address_state_province = TextField(
|
||||||
|
@ -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,13 +47,14 @@ 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 (type == "folders" and member["Account ID"] in folders[id]) or (
|
||||||
if member[flag]:
|
type != "folders" and member[flag]
|
||||||
ml.magic_save()
|
):
|
||||||
else:
|
ml.magic_save()
|
||||||
ml.delete_instance()
|
else:
|
||||||
|
ml.delete_instance()
|
||||||
|
|
||||||
print("Getting/Updating transactions...")
|
print("Getting/Updating transactions...")
|
||||||
# Deduping these is hard, so just recreate the data every time
|
# Deduping these is hard, so just recreate the data every time
|
||||||
@ -65,8 +72,12 @@ def do_import(config):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
for transaction in transactions:
|
Transaction.insert_many(
|
||||||
Transaction.from_csv_dict(transaction).magic_save()
|
[
|
||||||
|
Transaction.from_csv_dict(transaction).__data__
|
||||||
|
for transaction in transactions
|
||||||
|
]
|
||||||
|
).execute()
|
||||||
|
|
||||||
# TODO: folders, levels, addons
|
# TODO: folders, levels, addons
|
||||||
|
|
||||||
@ -75,7 +86,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
|
||||||
|
|
||||||
await user.save()
|
try:
|
||||||
|
await user.save()
|
||||||
|
except UdmError:
|
||||||
|
print("Failed to save user", username)
|
||||||
|
print(user.props)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
162
memberPlumbing/upcomingEvents.py
Normal file
162
memberPlumbing/upcomingEvents.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pyclip
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
TIME_FMT = "%l:%M%P"
|
||||||
|
DATETIME_FMT = "%a %b %e %Y, " + TIME_FMT
|
||||||
|
|
||||||
|
|
||||||
|
def format_datetime_range(start_ts: int, end_ts: int):
|
||||||
|
start = datetime.fromtimestamp(start_ts)
|
||||||
|
end = datetime.fromtimestamp(end_ts)
|
||||||
|
start_str = start.strftime(DATETIME_FMT)
|
||||||
|
if start.date() == end.date():
|
||||||
|
end_str = end.strftime(TIME_FMT)
|
||||||
|
else:
|
||||||
|
# TODO: this probably implies multiple instances. Should read
|
||||||
|
# RRULE or similar from the event notes
|
||||||
|
end_str = end.strftime(DATETIME_FMT)
|
||||||
|
return f"{start_str} — {end_str}"
|
||||||
|
|
||||||
|
|
||||||
|
def format_event(event_details, truncate: bool):
|
||||||
|
try:
|
||||||
|
url = (
|
||||||
|
"https://claremontmakerspace.org/events/#!event/register/"
|
||||||
|
+ event_details["url"]
|
||||||
|
)
|
||||||
|
if "lgo" in event_details:
|
||||||
|
img = f"""<img class="alignleft" width="400" src="{event_details['lgo']['l']}">"""
|
||||||
|
else:
|
||||||
|
img = ""
|
||||||
|
# print(json.dumps(event_details))
|
||||||
|
out = f"""<h2 style="text-align: center;">
|
||||||
|
<a href="{url}">{img}{event_details['ttl']}</a>
|
||||||
|
</h2>
|
||||||
|
<div><i>{format_datetime_range(event_details['sdp'], event_details['edp'])}</i></div>
|
||||||
|
"""
|
||||||
|
if not truncate:
|
||||||
|
out += f"""
|
||||||
|
<div>
|
||||||
|
{event_details['dtl']}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{url}">Register for this class now!</a>"""
|
||||||
|
return out
|
||||||
|
except KeyError as e:
|
||||||
|
print(
|
||||||
|
f"Event '{event_details.get('ttl')}' missing required property: '{e.args[0]}'"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def format_section(title: str, blurb: str, events, truncate: bool):
|
||||||
|
# skip empty sections
|
||||||
|
if not events:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
events_list = "\n<hr />\n\n".join(format_event(event, truncate) for event in events)
|
||||||
|
|
||||||
|
return f"""<h1>{title}</h1>
|
||||||
|
<h4><i>{blurb}</i></h4>
|
||||||
|
{events_list}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def generate_post():
|
||||||
|
config = Config()
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
membershipworks = config.membershipworks
|
||||||
|
events = membershipworks.get_events_list(now)
|
||||||
|
if "error" in events:
|
||||||
|
print("Error:", events["error"])
|
||||||
|
return
|
||||||
|
|
||||||
|
ongoing_events = []
|
||||||
|
full_events = []
|
||||||
|
upcoming_events = []
|
||||||
|
for event in events["evt"]:
|
||||||
|
try:
|
||||||
|
# ignore hidden events
|
||||||
|
if event["cal"] == 0:
|
||||||
|
continue
|
||||||
|
event_details = membershipworks.get_event_by_eid(event["eid"])
|
||||||
|
|
||||||
|
# registration has already ended
|
||||||
|
if (
|
||||||
|
"erd" in event_details
|
||||||
|
and datetime.fromtimestamp(event_details["erd"]) < now
|
||||||
|
):
|
||||||
|
ongoing_events.append(event_details)
|
||||||
|
# class is full
|
||||||
|
elif event_details["cnt"] >= event_details["cap"]:
|
||||||
|
full_events.append(event_details)
|
||||||
|
else:
|
||||||
|
upcoming_events.append(event_details)
|
||||||
|
except KeyError as e:
|
||||||
|
print(
|
||||||
|
f"Event '{event.get('ttl')}' missing required property: '{e.args[0]}'"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
# header
|
||||||
|
yield """<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>Instructors:</strong> Interested in teaching a class at CMS? Please fill out our <a href="https://claremontmakerspace.org/cms-class-proposal-form/" data-wpel-link="internal">Class Proposal Form</a>.
|
||||||
|
|
||||||
|
<strong>Tours:</strong> Want to see what the Claremont MakerSpace is all about? Tours are by appointment only. <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 />
|
||||||
|
"""
|
||||||
|
|
||||||
|
yield format_section(
|
||||||
|
"Upcoming Events",
|
||||||
|
"Events that are currently open for registration.",
|
||||||
|
upcoming_events,
|
||||||
|
truncate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield format_section(
|
||||||
|
"<hr />Just Missed",
|
||||||
|
"These classes are currently full at time of writing. If you are interested, please check the event's page; spots occasionally open up. Keep an eye on this newsletter to see when these classes are offered again.",
|
||||||
|
full_events,
|
||||||
|
truncate=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield format_section(
|
||||||
|
"<hr />Ongoing Events",
|
||||||
|
"These events are ongoing. Registration is currently closed, but these events may be offered again in the future.",
|
||||||
|
ongoing_events,
|
||||||
|
truncate=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# footer
|
||||||
|
yield """<div style="clear: both;">
|
||||||
|
<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>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
result = "\n".join(generate_post())
|
||||||
|
print(result)
|
||||||
|
pyclip.copy(result)
|
||||||
|
print("Copied to clipboard!", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
852
poetry.lock
generated
852
poetry.lock
generated
@ -1,852 +0,0 @@
|
|||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Async http client/server framework (asyncio)"
|
|
||||||
name = "aiohttp"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5.3"
|
|
||||||
version = "3.6.2"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
async-timeout = ">=3.0,<4.0"
|
|
||||||
attrs = ">=17.3.0"
|
|
||||||
chardet = ">=2.0,<4.0"
|
|
||||||
multidict = ">=4.5,<5.0"
|
|
||||||
yarl = ">=1.0,<2.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
speedups = ["aiodns", "brotlipy", "cchardet"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
|
||||||
name = "appdirs"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "1.4.4"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Python decorator for async properties."
|
|
||||||
name = "async-property"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "0.2.1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Timeout context manager for asyncio programs"
|
|
||||||
name = "async-timeout"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5.3"
|
|
||||||
version = "3.0.1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Atomic file writes."
|
|
||||||
marker = "sys_platform == \"win32\""
|
|
||||||
name = "atomicwrites"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
version = "1.4.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Classes Without Boilerplate"
|
|
||||||
name = "attrs"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
version = "19.3.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
|
|
||||||
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
|
|
||||||
docs = ["sphinx", "zope.interface"]
|
|
||||||
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Simple construction, analysis and modification of binary data."
|
|
||||||
name = "bitstring"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "3.1.7"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "The uncompromising code formatter."
|
|
||||||
name = "black"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
version = "19.10b0"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
appdirs = "*"
|
|
||||||
attrs = ">=18.1.0"
|
|
||||||
click = ">=6.5"
|
|
||||||
pathspec = ">=0.6,<1"
|
|
||||||
regex = "*"
|
|
||||||
toml = ">=0.9.4"
|
|
||||||
typed-ast = ">=1.4.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
|
||||||
name = "certifi"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "2020.6.20"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Universal encoding detector for Python 2 and 3"
|
|
||||||
name = "chardet"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "3.0.4"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Composable command line interface toolkit"
|
|
||||||
name = "click"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|
||||||
version = "7.1.2"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Cross-platform colored terminal text."
|
|
||||||
marker = "sys_platform == \"win32\""
|
|
||||||
name = "colorama"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|
||||||
version = "0.4.3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Code coverage measurement for Python"
|
|
||||||
name = "coverage"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
|
||||||
version = "5.2"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
[package.dependencies.toml]
|
|
||||||
optional = true
|
|
||||||
version = "*"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
toml = ["toml"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "A Python library for the Docker Engine API."
|
|
||||||
name = "docker"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|
||||||
version = "4.2.2"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
requests = ">=2.14.2,<2.18.0 || >2.18.0"
|
|
||||||
six = ">=1.4.0"
|
|
||||||
websocket-client = ">=0.32.0"
|
|
||||||
|
|
||||||
[package.dependencies.pypiwin32]
|
|
||||||
python = ">=3.6"
|
|
||||||
version = "223"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
ssh = ["paramiko (>=2.4.2)"]
|
|
||||||
tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
|
||||||
name = "idna"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
version = "2.10"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Read metadata from Python packages"
|
|
||||||
marker = "python_version < \"3.8\""
|
|
||||||
name = "importlib-metadata"
|
|
||||||
optional = false
|
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
|
||||||
version = "1.7.0"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
zipp = ">=0.5"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["sphinx", "rst.linker"]
|
|
||||||
testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "A Python utility / library to sort Python imports."
|
|
||||||
name = "isort"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
version = "4.3.21"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
pipfile = ["pipreqs", "requirementslib"]
|
|
||||||
pyproject = ["toml"]
|
|
||||||
requirements = ["pipreqs", "pip-api"]
|
|
||||||
xdg_home = ["appdirs (>=1.4.0)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
|
||||||
name = "lxml"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
|
|
||||||
version = "4.5.2"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
cssselect = ["cssselect (>=0.7)"]
|
|
||||||
html5 = ["html5lib"]
|
|
||||||
htmlsoup = ["beautifulsoup4"]
|
|
||||||
source = ["Cython (>=0.29.7)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "More routines for operating on iterables, beyond itertools"
|
|
||||||
name = "more-itertools"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5"
|
|
||||||
version = "8.4.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "multidict implementation"
|
|
||||||
name = "multidict"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5"
|
|
||||||
version = "4.7.6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Python interface to MySQL"
|
|
||||||
name = "mysqlclient"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "1.4.6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Core utilities for Python packages"
|
|
||||||
name = "packaging"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
version = "20.4"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
pyparsing = ">=2.0.2"
|
|
||||||
six = "*"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Utility library for gitignore style pattern matching of file paths."
|
|
||||||
name = "pathspec"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|
||||||
version = "0.8.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "a little orm"
|
|
||||||
name = "peewee"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "3.13.3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "plugin and hook calling mechanisms for python"
|
|
||||||
name = "pluggy"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
version = "0.13.1"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
[package.dependencies.importlib-metadata]
|
|
||||||
python = "<3.8"
|
|
||||||
version = ">=0.12"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
dev = ["pre-commit", "tox"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
|
||||||
name = "py"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
version = "1.9.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Python parsing module"
|
|
||||||
name = "pyparsing"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
|
||||||
version = "2.4.7"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = ""
|
|
||||||
marker = "sys_platform == \"win32\" and python_version >= \"3.6\""
|
|
||||||
name = "pypiwin32"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "223"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
pywin32 = ">=223"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "pytest: simple powerful testing with Python"
|
|
||||||
name = "pytest"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5"
|
|
||||||
version = "5.4.3"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
atomicwrites = ">=1.0"
|
|
||||||
attrs = ">=17.4.0"
|
|
||||||
colorama = "*"
|
|
||||||
more-itertools = ">=4.0.0"
|
|
||||||
packaging = "*"
|
|
||||||
pluggy = ">=0.12,<1.0"
|
|
||||||
py = ">=1.5.0"
|
|
||||||
wcwidth = "*"
|
|
||||||
|
|
||||||
[package.dependencies.importlib-metadata]
|
|
||||||
python = "<3.8"
|
|
||||||
version = ">=0.12"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
checkqa-mypy = ["mypy (v0.761)"]
|
|
||||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Python for Window Extensions"
|
|
||||||
marker = "sys_platform == \"win32\" and python_version >= \"3.6\""
|
|
||||||
name = "pywin32"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "228"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Alternative regular expression module, to replace re."
|
|
||||||
name = "regex"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "2020.7.14"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Python HTTP for Humans."
|
|
||||||
name = "requests"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|
||||||
version = "2.24.0"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
certifi = ">=2017.4.17"
|
|
||||||
chardet = ">=3.0.2,<4"
|
|
||||||
idna = ">=2.5,<3"
|
|
||||||
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
|
|
||||||
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
|
|
||||||
name = "ruamel.yaml"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "0.16.10"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
[package.dependencies."ruamel.yaml.clib"]
|
|
||||||
python = "<3.9"
|
|
||||||
version = ">=0.1.2"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["ryd"]
|
|
||||||
jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
|
|
||||||
marker = "platform_python_implementation == \"CPython\" and python_version < \"3.9\""
|
|
||||||
name = "ruamel.yaml.clib"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "0.2.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Python 2 and 3 compatibility utilities"
|
|
||||||
name = "six"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
|
||||||
version = "1.15.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "tasks runner for python projects"
|
|
||||||
name = "taskipy"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6,<4.0"
|
|
||||||
version = "1.2.1"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
toml = ">=0.10.0,<0.11.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
|
||||||
name = "toml"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "0.10.1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
|
||||||
name = "typed-ast"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "1.4.1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Python library to interact with the Univention UDM REST API. Implements the simple Python UDM API."
|
|
||||||
name = "udm-rest-client"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
version = "0.4.0"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
aiohttp = "*"
|
|
||||||
async-property = "*"
|
|
||||||
click = "*"
|
|
||||||
docker = "*"
|
|
||||||
requests = "*"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
|
||||||
name = "urllib3"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
|
||||||
version = "1.25.9"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
brotli = ["brotlipy (>=0.6.0)"]
|
|
||||||
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"]
|
|
||||||
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Measures the displayed width of unicode strings in a terminal"
|
|
||||||
name = "wcwidth"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "0.2.5"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "WebSocket client for Python. hybi13 is supported."
|
|
||||||
name = "websocket-client"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|
||||||
version = "0.57.0"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
six = "*"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Yet another URL library"
|
|
||||||
name = "yarl"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.5"
|
|
||||||
version = "1.4.2"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
idna = ">=2.0"
|
|
||||||
multidict = ">=4.0"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
|
||||||
marker = "python_version < \"3.8\""
|
|
||||||
name = "zipp"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.6"
|
|
||||||
version = "3.1.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
|
||||||
testing = ["jaraco.itertools", "func-timeout"]
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
content-hash = "ccab831ceb4e390d9ffa4d7768046fa520130a2888438f575721116de4db5314"
|
|
||||||
python-versions = "^3.7"
|
|
||||||
|
|
||||||
[metadata.files]
|
|
||||||
aiohttp = [
|
|
||||||
{file = "aiohttp-3.6.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e"},
|
|
||||||
{file = "aiohttp-3.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec"},
|
|
||||||
{file = "aiohttp-3.6.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48"},
|
|
||||||
{file = "aiohttp-3.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59"},
|
|
||||||
{file = "aiohttp-3.6.2-cp36-cp36m-win32.whl", hash = "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a"},
|
|
||||||
{file = "aiohttp-3.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17"},
|
|
||||||
{file = "aiohttp-3.6.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a"},
|
|
||||||
{file = "aiohttp-3.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd"},
|
|
||||||
{file = "aiohttp-3.6.2-cp37-cp37m-win32.whl", hash = "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"},
|
|
||||||
{file = "aiohttp-3.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654"},
|
|
||||||
{file = "aiohttp-3.6.2-py3-none-any.whl", hash = "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4"},
|
|
||||||
{file = "aiohttp-3.6.2.tar.gz", hash = "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326"},
|
|
||||||
]
|
|
||||||
appdirs = [
|
|
||||||
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
|
|
||||||
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
|
|
||||||
]
|
|
||||||
async-property = [
|
|
||||||
{file = "async_property-0.2.1-py2.py3-none-any.whl", hash = "sha256:f1f105009a6216ed9a13031aa13632754ed8a5c2e329fb8f9f2082d83529eacd"},
|
|
||||||
{file = "async_property-0.2.1.tar.gz", hash = "sha256:53826fd45a67d7d6cca3d7abbc0e8ba951f7c7618c830021fbd3675979b0b67d"},
|
|
||||||
]
|
|
||||||
async-timeout = [
|
|
||||||
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
|
|
||||||
{file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
|
|
||||||
]
|
|
||||||
atomicwrites = [
|
|
||||||
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
|
|
||||||
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
|
|
||||||
]
|
|
||||||
attrs = [
|
|
||||||
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
|
|
||||||
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
|
|
||||||
]
|
|
||||||
bitstring = [
|
|
||||||
{file = "bitstring-3.1.7.tar.gz", hash = "sha256:fdf3eb72b229d2864fb507f8f42b1b2c57af7ce5fec035972f9566de440a864a"},
|
|
||||||
]
|
|
||||||
black = [
|
|
||||||
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
|
|
||||||
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
|
|
||||||
]
|
|
||||||
certifi = [
|
|
||||||
{file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
|
|
||||||
{file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
|
|
||||||
]
|
|
||||||
chardet = [
|
|
||||||
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
|
|
||||||
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
|
|
||||||
]
|
|
||||||
click = [
|
|
||||||
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
|
|
||||||
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
|
|
||||||
]
|
|
||||||
colorama = [
|
|
||||||
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
|
|
||||||
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
|
|
||||||
]
|
|
||||||
coverage = [
|
|
||||||
{file = "coverage-5.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a"},
|
|
||||||
{file = "coverage-5.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10"},
|
|
||||||
{file = "coverage-5.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62"},
|
|
||||||
{file = "coverage-5.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613"},
|
|
||||||
{file = "coverage-5.2-cp27-cp27m-win32.whl", hash = "sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4"},
|
|
||||||
{file = "coverage-5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a"},
|
|
||||||
{file = "coverage-5.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70"},
|
|
||||||
{file = "coverage-5.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee"},
|
|
||||||
{file = "coverage-5.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b"},
|
|
||||||
{file = "coverage-5.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913"},
|
|
||||||
{file = "coverage-5.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c"},
|
|
||||||
{file = "coverage-5.2-cp35-cp35m-win32.whl", hash = "sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b"},
|
|
||||||
{file = "coverage-5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e"},
|
|
||||||
{file = "coverage-5.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0"},
|
|
||||||
{file = "coverage-5.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f"},
|
|
||||||
{file = "coverage-5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405"},
|
|
||||||
{file = "coverage-5.2-cp36-cp36m-win32.whl", hash = "sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40"},
|
|
||||||
{file = "coverage-5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e"},
|
|
||||||
{file = "coverage-5.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6"},
|
|
||||||
{file = "coverage-5.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1"},
|
|
||||||
{file = "coverage-5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d"},
|
|
||||||
{file = "coverage-5.2-cp37-cp37m-win32.whl", hash = "sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec"},
|
|
||||||
{file = "coverage-5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703"},
|
|
||||||
{file = "coverage-5.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032"},
|
|
||||||
{file = "coverage-5.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d"},
|
|
||||||
{file = "coverage-5.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e"},
|
|
||||||
{file = "coverage-5.2-cp38-cp38-win32.whl", hash = "sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7"},
|
|
||||||
{file = "coverage-5.2-cp38-cp38-win_amd64.whl", hash = "sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b"},
|
|
||||||
{file = "coverage-5.2-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d"},
|
|
||||||
{file = "coverage-5.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d"},
|
|
||||||
{file = "coverage-5.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c"},
|
|
||||||
{file = "coverage-5.2-cp39-cp39-win32.whl", hash = "sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c"},
|
|
||||||
{file = "coverage-5.2-cp39-cp39-win_amd64.whl", hash = "sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2"},
|
|
||||||
{file = "coverage-5.2.tar.gz", hash = "sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404"},
|
|
||||||
]
|
|
||||||
docker = [
|
|
||||||
{file = "docker-4.2.2-py2.py3-none-any.whl", hash = "sha256:03a46400c4080cb6f7aa997f881ddd84fef855499ece219d75fbdb53289c17ab"},
|
|
||||||
{file = "docker-4.2.2.tar.gz", hash = "sha256:26eebadce7e298f55b76a88c4f8802476c5eaddbdbe38dbc6cce8781c47c9b54"},
|
|
||||||
]
|
|
||||||
idna = [
|
|
||||||
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
|
|
||||||
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
|
|
||||||
]
|
|
||||||
importlib-metadata = [
|
|
||||||
{file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"},
|
|
||||||
{file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"},
|
|
||||||
]
|
|
||||||
isort = [
|
|
||||||
{file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"},
|
|
||||||
{file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"},
|
|
||||||
]
|
|
||||||
lxml = [
|
|
||||||
{file = "lxml-4.5.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:74f48ec98430e06c1fa8949b49ebdd8d27ceb9df8d3d1c92e1fdc2773f003f20"},
|
|
||||||
{file = "lxml-4.5.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e70d4e467e243455492f5de463b72151cc400710ac03a0678206a5f27e79ddef"},
|
|
||||||
{file = "lxml-4.5.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:7ad7906e098ccd30d8f7068030a0b16668ab8aa5cda6fcd5146d8d20cbaa71b5"},
|
|
||||||
{file = "lxml-4.5.2-cp27-cp27m-win32.whl", hash = "sha256:92282c83547a9add85ad658143c76a64a8d339028926d7dc1998ca029c88ea6a"},
|
|
||||||
{file = "lxml-4.5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f"},
|
|
||||||
{file = "lxml-4.5.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:94150231f1e90c9595ccc80d7d2006c61f90a5995db82bccbca7944fd457f0f6"},
|
|
||||||
{file = "lxml-4.5.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bea760a63ce9bba566c23f726d72b3c0250e2fa2569909e2d83cda1534c79443"},
|
|
||||||
{file = "lxml-4.5.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c3f511a3c58676147c277eff0224c061dd5a6a8e1373572ac817ac6324f1b1e0"},
|
|
||||||
{file = "lxml-4.5.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:59daa84aef650b11bccd18f99f64bfe44b9f14a08a28259959d33676554065a1"},
|
|
||||||
{file = "lxml-4.5.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:c9d317efde4bafbc1561509bfa8a23c5cab66c44d49ab5b63ff690f5159b2304"},
|
|
||||||
{file = "lxml-4.5.2-cp35-cp35m-win32.whl", hash = "sha256:9dc9006dcc47e00a8a6a029eb035c8f696ad38e40a27d073a003d7d1443f5d88"},
|
|
||||||
{file = "lxml-4.5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:08fc93257dcfe9542c0a6883a25ba4971d78297f63d7a5a26ffa34861ca78730"},
|
|
||||||
{file = "lxml-4.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:121b665b04083a1e85ff1f5243d4a93aa1aaba281bc12ea334d5a187278ceaf1"},
|
|
||||||
{file = "lxml-4.5.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5591c4164755778e29e69b86e425880f852464a21c7bb53c7ea453bbe2633bbe"},
|
|
||||||
{file = "lxml-4.5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cc411ad324a4486b142c41d9b2b6a722c534096963688d879ea6fa8a35028258"},
|
|
||||||
{file = "lxml-4.5.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:1fa21263c3aba2b76fd7c45713d4428dbcc7644d73dcf0650e9d344e433741b3"},
|
|
||||||
{file = "lxml-4.5.2-cp36-cp36m-win32.whl", hash = "sha256:786aad2aa20de3dbff21aab86b2fb6a7be68064cbbc0219bde414d3a30aa47ae"},
|
|
||||||
{file = "lxml-4.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:e1cacf4796b20865789083252186ce9dc6cc59eca0c2e79cca332bdff24ac481"},
|
|
||||||
{file = "lxml-4.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:80a38b188d20c0524fe8959c8ce770a8fdf0e617c6912d23fc97c68301bb9aba"},
|
|
||||||
{file = "lxml-4.5.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd"},
|
|
||||||
{file = "lxml-4.5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a76979f728dd845655026ab991df25d26379a1a8fc1e9e68e25c7eda43004bed"},
|
|
||||||
{file = "lxml-4.5.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:cfd7c5dd3c35c19cec59c63df9571c67c6d6e5c92e0fe63517920e97f61106d1"},
|
|
||||||
{file = "lxml-4.5.2-cp37-cp37m-win32.whl", hash = "sha256:5a9c8d11aa2c8f8b6043d845927a51eb9102eb558e3f936df494e96393f5fd3e"},
|
|
||||||
{file = "lxml-4.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4b4a111bcf4b9c948e020fd207f915c24a6de3f1adc7682a2d92660eb4e84f1a"},
|
|
||||||
{file = "lxml-4.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5dd20538a60c4cc9a077d3b715bb42307239fcd25ef1ca7286775f95e9e9a46d"},
|
|
||||||
{file = "lxml-4.5.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:2b30aa2bcff8e958cd85d907d5109820b01ac511eae5b460803430a7404e34d7"},
|
|
||||||
{file = "lxml-4.5.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:aa8eba3db3d8761db161003e2d0586608092e217151d7458206e243be5a43843"},
|
|
||||||
{file = "lxml-4.5.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f0ec6b9b3832e0bd1d57af41f9238ea7709bbd7271f639024f2fc9d3bb01293"},
|
|
||||||
{file = "lxml-4.5.2-cp38-cp38-win32.whl", hash = "sha256:107781b213cf7201ec3806555657ccda67b1fccc4261fb889ef7fc56976db81f"},
|
|
||||||
{file = "lxml-4.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee"},
|
|
||||||
{file = "lxml-4.5.2.tar.gz", hash = "sha256:cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6"},
|
|
||||||
]
|
|
||||||
more-itertools = [
|
|
||||||
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
|
|
||||||
{file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"},
|
|
||||||
]
|
|
||||||
multidict = [
|
|
||||||
{file = "multidict-4.7.6-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000"},
|
|
||||||
{file = "multidict-4.7.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a"},
|
|
||||||
{file = "multidict-4.7.6-cp35-cp35m-win32.whl", hash = "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5"},
|
|
||||||
{file = "multidict-4.7.6-cp35-cp35m-win_amd64.whl", hash = "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3"},
|
|
||||||
{file = "multidict-4.7.6-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87"},
|
|
||||||
{file = "multidict-4.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2"},
|
|
||||||
{file = "multidict-4.7.6-cp36-cp36m-win32.whl", hash = "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7"},
|
|
||||||
{file = "multidict-4.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463"},
|
|
||||||
{file = "multidict-4.7.6-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"},
|
|
||||||
{file = "multidict-4.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255"},
|
|
||||||
{file = "multidict-4.7.6-cp37-cp37m-win32.whl", hash = "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507"},
|
|
||||||
{file = "multidict-4.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c"},
|
|
||||||
{file = "multidict-4.7.6-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b"},
|
|
||||||
{file = "multidict-4.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7"},
|
|
||||||
{file = "multidict-4.7.6-cp38-cp38-win32.whl", hash = "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d"},
|
|
||||||
{file = "multidict-4.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19"},
|
|
||||||
{file = "multidict-4.7.6.tar.gz", hash = "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430"},
|
|
||||||
]
|
|
||||||
mysqlclient = [
|
|
||||||
{file = "mysqlclient-1.4.6-cp36-cp36m-win_amd64.whl", hash = "sha256:4c82187dd6ab3607150fbb1fa5ef4643118f3da122b8ba31c3149ddd9cf0cb39"},
|
|
||||||
{file = "mysqlclient-1.4.6-cp37-cp37m-win_amd64.whl", hash = "sha256:9e6080a7aee4cc6a06b58b59239f20f1d259c1d2fddf68ddeed242d2311c7087"},
|
|
||||||
{file = "mysqlclient-1.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:f646f8d17d02be0872291f258cce3813497bc7888cd4712a577fd1e719b2f213"},
|
|
||||||
{file = "mysqlclient-1.4.6.tar.gz", hash = "sha256:f3fdaa9a38752a3b214a6fe79d7cae3653731a53e577821f9187e67cbecb2e16"},
|
|
||||||
]
|
|
||||||
packaging = [
|
|
||||||
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
|
|
||||||
{file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
|
|
||||||
]
|
|
||||||
pathspec = [
|
|
||||||
{file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"},
|
|
||||||
{file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"},
|
|
||||||
]
|
|
||||||
peewee = [
|
|
||||||
{file = "peewee-3.13.3.tar.gz", hash = "sha256:1269a9736865512bd4056298003aab190957afe07d2616cf22eaf56cb6398369"},
|
|
||||||
]
|
|
||||||
pluggy = [
|
|
||||||
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
|
|
||||||
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
|
|
||||||
]
|
|
||||||
py = [
|
|
||||||
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
|
|
||||||
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
|
|
||||||
]
|
|
||||||
pyparsing = [
|
|
||||||
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
|
|
||||||
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
|
|
||||||
]
|
|
||||||
pypiwin32 = [
|
|
||||||
{file = "pypiwin32-223-py3-none-any.whl", hash = "sha256:67adf399debc1d5d14dffc1ab5acacb800da569754fafdc576b2a039485aa775"},
|
|
||||||
{file = "pypiwin32-223.tar.gz", hash = "sha256:71be40c1fbd28594214ecaecb58e7aa8b708eabfa0125c8a109ebd51edbd776a"},
|
|
||||||
]
|
|
||||||
pytest = [
|
|
||||||
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
|
|
||||||
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
|
|
||||||
]
|
|
||||||
pywin32 = [
|
|
||||||
{file = "pywin32-228-cp27-cp27m-win32.whl", hash = "sha256:37dc9935f6a383cc744315ae0c2882ba1768d9b06700a70f35dc1ce73cd4ba9c"},
|
|
||||||
{file = "pywin32-228-cp27-cp27m-win_amd64.whl", hash = "sha256:11cb6610efc2f078c9e6d8f5d0f957620c333f4b23466931a247fb945ed35e89"},
|
|
||||||
{file = "pywin32-228-cp35-cp35m-win32.whl", hash = "sha256:1f45db18af5d36195447b2cffacd182fe2d296849ba0aecdab24d3852fbf3f80"},
|
|
||||||
{file = "pywin32-228-cp35-cp35m-win_amd64.whl", hash = "sha256:6e38c44097a834a4707c1b63efa9c2435f5a42afabff634a17f563bc478dfcc8"},
|
|
||||||
{file = "pywin32-228-cp36-cp36m-win32.whl", hash = "sha256:ec16d44b49b5f34e99eb97cf270806fdc560dff6f84d281eb2fcb89a014a56a9"},
|
|
||||||
{file = "pywin32-228-cp36-cp36m-win_amd64.whl", hash = "sha256:a60d795c6590a5b6baeacd16c583d91cce8038f959bd80c53bd9a68f40130f2d"},
|
|
||||||
{file = "pywin32-228-cp37-cp37m-win32.whl", hash = "sha256:af40887b6fc200eafe4d7742c48417529a8702dcc1a60bf89eee152d1d11209f"},
|
|
||||||
{file = "pywin32-228-cp37-cp37m-win_amd64.whl", hash = "sha256:00eaf43dbd05ba6a9b0080c77e161e0b7a601f9a3f660727a952e40140537de7"},
|
|
||||||
{file = "pywin32-228-cp38-cp38-win32.whl", hash = "sha256:fa6ba028909cfc64ce9e24bcf22f588b14871980d9787f1e2002c99af8f1850c"},
|
|
||||||
{file = "pywin32-228-cp38-cp38-win_amd64.whl", hash = "sha256:9b3466083f8271e1a5eb0329f4e0d61925d46b40b195a33413e0905dccb285e8"},
|
|
||||||
{file = "pywin32-228-cp39-cp39-win32.whl", hash = "sha256:ed74b72d8059a6606f64842e7917aeee99159ebd6b8d6261c518d002837be298"},
|
|
||||||
{file = "pywin32-228-cp39-cp39-win_amd64.whl", hash = "sha256:8319bafdcd90b7202c50d6014efdfe4fde9311b3ff15fd6f893a45c0868de203"},
|
|
||||||
]
|
|
||||||
regex = [
|
|
||||||
{file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"},
|
|
||||||
{file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"},
|
|
||||||
{file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"},
|
|
||||||
{file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"},
|
|
||||||
{file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"},
|
|
||||||
{file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"},
|
|
||||||
{file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"},
|
|
||||||
{file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"},
|
|
||||||
{file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"},
|
|
||||||
{file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"},
|
|
||||||
{file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"},
|
|
||||||
{file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"},
|
|
||||||
{file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"},
|
|
||||||
{file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"},
|
|
||||||
{file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"},
|
|
||||||
{file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"},
|
|
||||||
{file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"},
|
|
||||||
{file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"},
|
|
||||||
{file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"},
|
|
||||||
{file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"},
|
|
||||||
{file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"},
|
|
||||||
]
|
|
||||||
requests = [
|
|
||||||
{file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
|
|
||||||
{file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
|
|
||||||
]
|
|
||||||
"ruamel.yaml" = [
|
|
||||||
{file = "ruamel.yaml-0.16.10-py2.py3-none-any.whl", hash = "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b"},
|
|
||||||
{file = "ruamel.yaml-0.16.10.tar.gz", hash = "sha256:099c644a778bf72ffa00524f78dd0b6476bca94a1da344130f4bf3381ce5b954"},
|
|
||||||
]
|
|
||||||
"ruamel.yaml.clib" = [
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c6d040d0396c28d3eaaa6cb20152cb3b2f15adf35a0304f4f40a3cf9f1d2448"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d55386129291b96483edcb93b381470f7cd69f97585829b048a3d758d31210a"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win32.whl", hash = "sha256:8073c8b92b06b572e4057b583c3d01674ceaf32167801fe545a087d7a1e8bf52"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win_amd64.whl", hash = "sha256:615b0396a7fad02d1f9a0dcf9f01202bf9caefee6265198f252c865f4227fcc6"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:77556a7aa190be9a2bd83b7ee075d3df5f3c5016d395613671487e79b082d784"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win32.whl", hash = "sha256:392b7c371312abf27fb549ec2d5e0092f7ef6e6c9f767bfb13e83cb903aca0fd"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7aee724e1ff424757b5bd8f6c5bbdb033a570b2b4683b17ace4dbe61a99a657b"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win32.whl", hash = "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e77424825caba5553bbade750cec2277ef130647d685c2b38f68bc03453bac6"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:550168c02d8de52ee58c3d8a8193d5a8a9491a5e7b2462d27ac5bf63717574c9"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win32.whl", hash = "sha256:57933a6986a3036257ad7bf283529e7c19c2810ff24c86f4a0cfeb49d2099919"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:be018933c2f4ee7de55e7bd7d0d801b3dfb09d21dad0cce8a97995fd3e44be30"},
|
|
||||||
{file = "ruamel.yaml.clib-0.2.0.tar.gz", hash = "sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c"},
|
|
||||||
]
|
|
||||||
six = [
|
|
||||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
|
||||||
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
|
||||||
]
|
|
||||||
taskipy = [
|
|
||||||
{file = "taskipy-1.2.1-py3-none-any.whl", hash = "sha256:99bdaf5b19791c2345806847147e0fc2d28e1ac9446058def5a8b6b3fc9f23e2"},
|
|
||||||
{file = "taskipy-1.2.1.tar.gz", hash = "sha256:5eb2c3b1606c896c7fa799848e71e8883b880759224958d07ba760e5db263175"},
|
|
||||||
]
|
|
||||||
toml = [
|
|
||||||
{file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"},
|
|
||||||
{file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"},
|
|
||||||
]
|
|
||||||
typed-ast = [
|
|
||||||
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
|
|
||||||
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
|
|
||||||
{file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
|
|
||||||
{file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
|
|
||||||
{file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
|
|
||||||
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
|
|
||||||
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
|
|
||||||
{file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
|
|
||||||
{file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
|
|
||||||
{file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
|
|
||||||
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
|
|
||||||
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
|
|
||||||
{file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
|
|
||||||
{file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
|
|
||||||
{file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
|
|
||||||
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
|
|
||||||
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
|
|
||||||
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
|
|
||||||
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
|
|
||||||
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
|
|
||||||
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
|
|
||||||
]
|
|
||||||
udm-rest-client = [
|
|
||||||
{file = "udm-rest-client-0.4.0.tar.gz", hash = "sha256:3402a2320b6f9da8a2e5c307f90f90e40e83df9942e39a0e642c1367bd014b3e"},
|
|
||||||
{file = "udm_rest_client-0.4.0-py2.py3-none-any.whl", hash = "sha256:048f15050189d96070965a527fd185a9b72230b3c5f714fb1127c95c614dd7bf"},
|
|
||||||
]
|
|
||||||
urllib3 = [
|
|
||||||
{file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"},
|
|
||||||
{file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"},
|
|
||||||
]
|
|
||||||
wcwidth = [
|
|
||||||
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
|
|
||||||
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
|
|
||||||
]
|
|
||||||
websocket-client = [
|
|
||||||
{file = "websocket_client-0.57.0-py2.py3-none-any.whl", hash = "sha256:0fc45c961324d79c781bab301359d5a1b00b13ad1b10415a4780229ef71a5549"},
|
|
||||||
{file = "websocket_client-0.57.0.tar.gz", hash = "sha256:d735b91d6d1692a6a181f2a8c9e0238e5f6373356f561bb9dc4c7af36f452010"},
|
|
||||||
]
|
|
||||||
yarl = [
|
|
||||||
{file = "yarl-1.4.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b"},
|
|
||||||
{file = "yarl-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1"},
|
|
||||||
{file = "yarl-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080"},
|
|
||||||
{file = "yarl-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a"},
|
|
||||||
{file = "yarl-1.4.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f"},
|
|
||||||
{file = "yarl-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea"},
|
|
||||||
{file = "yarl-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb"},
|
|
||||||
{file = "yarl-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70"},
|
|
||||||
{file = "yarl-1.4.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d"},
|
|
||||||
{file = "yarl-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce"},
|
|
||||||
{file = "yarl-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"},
|
|
||||||
{file = "yarl-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce"},
|
|
||||||
{file = "yarl-1.4.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b"},
|
|
||||||
{file = "yarl-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae"},
|
|
||||||
{file = "yarl-1.4.2-cp38-cp38-win32.whl", hash = "sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462"},
|
|
||||||
{file = "yarl-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6"},
|
|
||||||
{file = "yarl-1.4.2.tar.gz", hash = "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b"},
|
|
||||||
]
|
|
||||||
zipp = [
|
|
||||||
{file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},
|
|
||||||
{file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"},
|
|
||||||
]
|
|
@ -1,4 +1,48 @@
|
|||||||
|
[project]
|
||||||
|
name = "memberPlumbing"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = [
|
||||||
|
{name = "Adam Goldsmith", email = "adam@adamgoldsmith.name"},
|
||||||
|
]
|
||||||
|
requires-python = ">=3.11,<4.0"
|
||||||
|
dependencies = [
|
||||||
|
"requests~=2.31",
|
||||||
|
"ruamel-yaml~=0.17",
|
||||||
|
"bitstring~=4.1",
|
||||||
|
"lxml~=4.9",
|
||||||
|
"peewee~=3.16",
|
||||||
|
"mysqlclient~=2.1",
|
||||||
|
"udm-rest-client~=1.2",
|
||||||
|
"pyclip~=0.7",
|
||||||
|
"recurrent~=0.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pdm]
|
||||||
|
[tool.pdm.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
"black~=23.3",
|
||||||
|
"isort~=5.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pdm.build]
|
||||||
|
includes = [
|
||||||
|
"memberPlumbing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["pdm-backend"]
|
||||||
|
build-backend = "pdm.backend"
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
doorUpdater = "memberPlumbing.doorUpdater:main"
|
||||||
|
hidEvents = "memberPlumbing.hidEvents:main"
|
||||||
|
sqlExport = "memberPlumbing.sqlExport:main"
|
||||||
|
ucsAccounts = "memberPlumbing.ucsAccounts:main"
|
||||||
|
upcomingEvents = "memberPlumbing.upcomingEvents:main"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
|
line-length = 88
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
multi_line_output = 3
|
multi_line_output = 3
|
||||||
@ -6,51 +50,3 @@ include_trailing_comma = true
|
|||||||
force_grid_wrap = 0
|
force_grid_wrap = 0
|
||||||
use_parentheses = true
|
use_parentheses = true
|
||||||
line_length = 88
|
line_length = 88
|
||||||
|
|
||||||
[tool.poetry]
|
|
||||||
name = "memberPlumbing"
|
|
||||||
packages = [{ include = "memberPlumbing" }]
|
|
||||||
version = "0.1.0"
|
|
||||||
description = ""
|
|
||||||
authors = ["Adam Goldsmith <adam@adamgoldsmith.name>"]
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
|
||||||
python = "^3.7"
|
|
||||||
requests = "^2.23.0"
|
|
||||||
"ruamel.yaml" = "^0.16.10"
|
|
||||||
bitstring = "^3.1.6"
|
|
||||||
lxml = "^4.5.0"
|
|
||||||
peewee = "^3.13.2"
|
|
||||||
mysqlclient = "^1.4.6"
|
|
||||||
udm-rest-client = "^0.4.0"
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
|
||||||
black = "^19.10b0"
|
|
||||||
isort = "^4.3.21"
|
|
||||||
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]
|
|
||||||
doorUpdater = 'memberPlumbing.doorUpdater:main'
|
|
||||||
hidEvents = 'memberPlumbing.hidEvents:main'
|
|
||||||
sqlExport = 'memberPlumbing.sqlExport:main'
|
|
||||||
ucsAccounts = 'memberPlumbing.ucsAccounts:main'
|
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry>=0.12"]
|
|
||||||
build-backend = "poetry.masonry.api"
|
|
||||||
|
6
renovate.json
Normal file
6
renovate.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
]
|
||||||
|
}
|
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