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 |
@ -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.
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from memberPlumbing import doorUpdater
|
|
||||||
|
|
||||||
doorUpdater.main()
|
|
@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from memberPlumbing import hidEvents
|
|
||||||
|
|
||||||
hidEvents.main()
|
|
@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from memberPlumbing import sqlExport
|
|
||||||
|
|
||||||
sqlExport.main()
|
|
@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from memberPlumbing import ucsAccounts
|
|
||||||
|
|
||||||
ucsAccounts.main()
|
|
@ -34,6 +34,12 @@ DOOR_PASSWORD: ""
|
|||||||
MEMBERSHIPWORKS_USERNAME: ""
|
MEMBERSHIPWORKS_USERNAME: ""
|
||||||
MEMBERSHIPWORKS_PASSWORD: ""
|
MEMBERSHIPWORKS_PASSWORD: ""
|
||||||
|
|
||||||
|
# arguments for https://udm-rest-client.readthedocs.io/en/latest/udm_rest_client.html#udm_rest_client.udm.UDM
|
||||||
|
UCS:
|
||||||
|
url: ""
|
||||||
|
username: ""
|
||||||
|
password: ""
|
||||||
|
|
||||||
MEMBERSHIPWORKS_DB:
|
MEMBERSHIPWORKS_DB:
|
||||||
database: ""
|
database: ""
|
||||||
user: ""
|
user: ""
|
||||||
|
@ -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()
|
||||||
|
@ -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}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@ -342,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(
|
||||||
|
@ -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,106 +1,130 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import asyncio
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
import subprocess
|
|
||||||
|
from udm_rest_client.udm import UDM
|
||||||
|
from udm_rest_client.exceptions import NoObject, UdmError
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
LDAP_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
|
||||||
|
|
||||||
|
|
||||||
def makeGroups(members):
|
# From an API error message:
|
||||||
|
# A group name must start and end with a letter, number or underscore.
|
||||||
|
# In between additionally spaces, dashes and dots are allowed.
|
||||||
|
def sanitize_group_name(name):
|
||||||
|
sanitized_body = re.sub(r"[^0-9A-Za-z_ -.]", ".", name)
|
||||||
|
sanitized_start_end = re.sub("^[^0-9A-Za-z_]|[^0-9A-Za-z_]$", "_", sanitized_body)
|
||||||
|
|
||||||
|
return "MW_" + sanitized_start_end
|
||||||
|
|
||||||
|
|
||||||
|
# From an API error message: "Username must only contain numbers, letters and dots!"
|
||||||
|
def sanitize_user_name(name):
|
||||||
|
return re.sub(r"[^0-9a-z.]", ".", name.lower()).strip(".")
|
||||||
|
|
||||||
|
|
||||||
|
async def make_groups(group_mod, members):
|
||||||
|
existing_group_names = [g.props.name async for g in group_mod.search()]
|
||||||
|
|
||||||
groups = [
|
groups = [
|
||||||
key.replace(":", ".").replace("?", "")
|
sanitize_group_name(group_name)
|
||||||
for key in members[0].keys()
|
for group_name in members[0].keys()
|
||||||
if re.match(GROUPS_REGEX, key) is not None
|
if re.match(GROUPS_REGEX, group_name) is not None
|
||||||
]
|
]
|
||||||
for group in groups:
|
for group_name in groups:
|
||||||
subprocess.call(
|
if group_name not in existing_group_names:
|
||||||
[
|
group = await group_mod.new()
|
||||||
"udm",
|
group.props.name = group_name
|
||||||
"groups/group",
|
await group.save()
|
||||||
"create",
|
|
||||||
"--position",
|
|
||||||
GROUP_BASE,
|
|
||||||
"--set",
|
|
||||||
"name=" + group,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def makeSets(props):
|
async def _main():
|
||||||
return sum([["--set", key + "=" + value] for key, value in props.items()], [])
|
|
||||||
|
|
||||||
|
|
||||||
def makeAppendGroups(member):
|
|
||||||
groups = [
|
|
||||||
key.replace(":", ".").replace("?", "")
|
|
||||||
for key, value in member.items()
|
|
||||||
if re.match(GROUPS_REGEX, key) is not None and value != ""
|
|
||||||
]
|
|
||||||
return sum(
|
|
||||||
[["--append", "groups=cn=" + group + "," + GROUP_BASE] for group in groups], []
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
config = Config()
|
config = Config()
|
||||||
|
|
||||||
members = config.membershipworks.get_members(
|
members = config.membershipworks.get_members(
|
||||||
["Members", "CMS Staff"], "lvl,phn,eml,lbl,nam,end,_id"
|
["Members", "CMS Staff"], "lvl,phn,eml,lbl,nam,end,_id"
|
||||||
)
|
)
|
||||||
makeGroups(members)
|
|
||||||
|
|
||||||
for member in members:
|
async with UDM(**config.UCS) as udm:
|
||||||
randomPass = "".join(
|
user_mod = udm.get("users/user")
|
||||||
random.choice(string.ascii_letters + string.digits)
|
group_mod = udm.get("groups/group")
|
||||||
for x in range(0, RAND_PW_LEN)
|
|
||||||
)
|
|
||||||
username = member["Account Name"].lower().replace(" ", ".")
|
|
||||||
|
|
||||||
props = {
|
await make_groups(group_mod, members)
|
||||||
"title": "", # Title
|
|
||||||
"firstname": member["First Name"],
|
|
||||||
"lastname": member["Last Name"], # (c)
|
|
||||||
"username": username, # (cmr)
|
|
||||||
"description": "", # Description
|
|
||||||
"password": randomPass, # (c) Password
|
|
||||||
# "mailPrimaryAddress": member["Email"], # Primary e-mail address
|
|
||||||
# "displayName": "", # Display name
|
|
||||||
# "birthday": "", # Birthdate
|
|
||||||
# "jpegPhoto": "", # Picture of the user (JPEG format)
|
|
||||||
"employeeNumber": member["Account ID"],
|
|
||||||
# "employeeType": "", # Employee type
|
|
||||||
"homedrive": "H:", # Windows home drive
|
|
||||||
"sambahome": "\\\\ucs\\" + username, # Windows home path
|
|
||||||
"profilepath": "%LOGONSERVER%\\%USERNAME%\\windows-profiles\\default", # Windows profile directory
|
|
||||||
"disabled": "1" if member["Account on Hold"] != "" else "0",
|
|
||||||
# "userexpiry": member["Renewal Date"],
|
|
||||||
"pwdChangeNextLogin": "1", # User has to change password on next login
|
|
||||||
# "sambaLogonHours": "", # Permitted times for Windows logins
|
|
||||||
"e-mail": member["Email"], # ([]) E-mail address
|
|
||||||
"phone": member["Phone"], # Telephone number
|
|
||||||
# "PasswordRecoveryMobile": member["Phone"], # Mobile phone number
|
|
||||||
"PasswordRecoveryEmail": member["Email"],
|
|
||||||
}
|
|
||||||
|
|
||||||
subprocess.call(
|
for member in members:
|
||||||
["udm", "users/user", "create", "--position", LDAP_BASE] + makeSets(props)
|
username = sanitize_user_name(member["Account Name"])
|
||||||
)
|
|
||||||
|
|
||||||
# remove props we don't want to reset
|
try: # try to get an existing user to update
|
||||||
props.pop("password")
|
user = await user_mod.get(f"uid={username},{USER_BASE}")
|
||||||
props.pop("pwdChangeNextLogin")
|
except NoObject: # create a new user
|
||||||
|
# TODO: search by employeeNumber and rename users when needed
|
||||||
|
user = await user_mod.new()
|
||||||
|
|
||||||
subprocess.call(
|
# set a random password and ensure it is changed at next login
|
||||||
["udm", "users/user", "modify", "--dn", "uid=" + username + "," + LDAP_BASE]
|
user.props.password = "".join(
|
||||||
+ makeSets(props)
|
random.choice(string.ascii_letters + string.digits)
|
||||||
+ makeAppendGroups(member)
|
for x in range(0, RAND_PW_LEN)
|
||||||
)
|
)
|
||||||
|
user.props.pwdChangeNextLogin = True
|
||||||
|
|
||||||
|
user.props.update(
|
||||||
|
{
|
||||||
|
"title": "", # Title
|
||||||
|
"firstname": member["First Name"],
|
||||||
|
"lastname": member["Last Name"], # (c)
|
||||||
|
"username": username, # (cmr)
|
||||||
|
"description": "", # Description
|
||||||
|
# "password": "", # (c) Password
|
||||||
|
# "mailPrimaryAddress": member["Email"], # Primary e-mail address
|
||||||
|
# "displayName": "", # Display name
|
||||||
|
# "birthday": "", # Birthdate
|
||||||
|
# "jpegPhoto": "", # Picture of the user (JPEG format)
|
||||||
|
"employeeNumber": member["Account ID"],
|
||||||
|
# "employeeType": "", # Employee type
|
||||||
|
"homedrive": "H:", # Windows home drive
|
||||||
|
"sambahome": "\\\\ucs\\" + username, # Windows home path
|
||||||
|
"profilepath": "%LOGONSERVER%\\%USERNAME%\\windows-profiles\\default", # Windows profile directory
|
||||||
|
"disabled": member["Account on Hold"] != "",
|
||||||
|
# "userexpiry": member["Renewal Date"],
|
||||||
|
# "pwdChangeNextLogin": "1", # User has to change password on next login
|
||||||
|
# "sambaLogonHours": "", # Permitted times for Windows logins
|
||||||
|
"e-mail": [member["Email"]], # ([]) E-mail address
|
||||||
|
"phone": [member["Phone"]], # Telephone number
|
||||||
|
# "PasswordRecoveryMobile": member["Phone"], # Mobile phone number
|
||||||
|
"PasswordRecoveryEmail": member["Email"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
new_groups = [
|
||||||
|
"cn=" + sanitize_group_name(group) + "," + GROUP_BASE
|
||||||
|
for group, value in member.items()
|
||||||
|
if re.match(GROUPS_REGEX, group) is not None and value != ""
|
||||||
|
]
|
||||||
|
# groups not from this script
|
||||||
|
other_old_groups = [
|
||||||
|
g for g in user.props.groups if not g[3:].startswith("MW_")
|
||||||
|
]
|
||||||
|
user.props.groups = other_old_groups + new_groups
|
||||||
|
|
||||||
|
try:
|
||||||
|
await user.save()
|
||||||
|
except UdmError:
|
||||||
|
print("Failed to save user", username)
|
||||||
|
print(user.props)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
asyncio.run(_main())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__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()
|
383
poetry.lock
generated
383
poetry.lock
generated
@ -1,383 +0,0 @@
|
|||||||
[[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.3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
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.6"
|
|
||||||
|
|
||||||
[[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.4.5.1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Universal encoding detector for Python 2 and 3"
|
|
||||||
name = "chardet"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "3.0.4"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "dev"
|
|
||||||
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.1"
|
|
||||||
|
|
||||||
[[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.9"
|
|
||||||
|
|
||||||
[[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.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
cssselect = ["cssselect (>=0.7)"]
|
|
||||||
html5 = ["html5lib"]
|
|
||||||
htmlsoup = ["beautifulsoup4"]
|
|
||||||
source = ["Cython (>=0.29.7)"]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Python interface to MySQL"
|
|
||||||
name = "mysqlclient"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "1.4.6"
|
|
||||||
|
|
||||||
[[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 = "Alternative regular expression module, to replace re."
|
|
||||||
name = "regex"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "2020.4.4"
|
|
||||||
|
|
||||||
[[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.23.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 = "dev"
|
|
||||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
|
||||||
name = "toml"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "0.10.0"
|
|
||||||
|
|
||||||
[[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 = "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)"]
|
|
||||||
|
|
||||||
[metadata]
|
|
||||||
content-hash = "5fe506efe8fd4c42bb9ef248033a7bd074d9f66e5b6a03727aa9cc554ae1d8fb"
|
|
||||||
python-versions = "^3.7"
|
|
||||||
|
|
||||||
[metadata.files]
|
|
||||||
appdirs = [
|
|
||||||
{file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"},
|
|
||||||
{file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"},
|
|
||||||
]
|
|
||||||
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.6-py2-none-any.whl", hash = "sha256:e392819965e7e0246e3cf6a51d5a54e731890ae03ebbfa3cd0e4f74909072096"},
|
|
||||||
{file = "bitstring-3.1.6-py3-none-any.whl", hash = "sha256:7b60b0c300d0d3d0a24ec84abfda4b0eaed3dc56dc90f6cbfe497166c9ad8443"},
|
|
||||||
{file = "bitstring-3.1.6.tar.gz", hash = "sha256:c97a8e2a136e99b523b27da420736ae5cb68f83519d633794a6a11192f69f8bf"},
|
|
||||||
]
|
|
||||||
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.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"},
|
|
||||||
{file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"},
|
|
||||||
]
|
|
||||||
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.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
|
|
||||||
{file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
|
|
||||||
]
|
|
||||||
idna = [
|
|
||||||
{file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
|
|
||||||
{file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
|
|
||||||
]
|
|
||||||
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.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c"},
|
|
||||||
{file = "lxml-4.5.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd"},
|
|
||||||
{file = "lxml-4.5.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261"},
|
|
||||||
{file = "lxml-4.5.0-cp27-cp27m-win32.whl", hash = "sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89"},
|
|
||||||
{file = "lxml-4.5.0-cp27-cp27m-win_amd64.whl", hash = "sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a"},
|
|
||||||
{file = "lxml-4.5.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128"},
|
|
||||||
{file = "lxml-4.5.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca"},
|
|
||||||
{file = "lxml-4.5.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb"},
|
|
||||||
{file = "lxml-4.5.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8"},
|
|
||||||
{file = "lxml-4.5.0-cp35-cp35m-win32.whl", hash = "sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77"},
|
|
||||||
{file = "lxml-4.5.0-cp35-cp35m-win_amd64.whl", hash = "sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081"},
|
|
||||||
{file = "lxml-4.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9"},
|
|
||||||
{file = "lxml-4.5.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717"},
|
|
||||||
{file = "lxml-4.5.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15"},
|
|
||||||
{file = "lxml-4.5.0-cp36-cp36m-win32.whl", hash = "sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7"},
|
|
||||||
{file = "lxml-4.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012"},
|
|
||||||
{file = "lxml-4.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6"},
|
|
||||||
{file = "lxml-4.5.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679"},
|
|
||||||
{file = "lxml-4.5.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc"},
|
|
||||||
{file = "lxml-4.5.0-cp37-cp37m-win32.whl", hash = "sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a"},
|
|
||||||
{file = "lxml-4.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8"},
|
|
||||||
{file = "lxml-4.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72"},
|
|
||||||
{file = "lxml-4.5.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1"},
|
|
||||||
{file = "lxml-4.5.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a"},
|
|
||||||
{file = "lxml-4.5.0-cp38-cp38-win32.whl", hash = "sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f"},
|
|
||||||
{file = "lxml-4.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3"},
|
|
||||||
{file = "lxml-4.5.0.tar.gz", hash = "sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60"},
|
|
||||||
]
|
|
||||||
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"},
|
|
||||||
]
|
|
||||||
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"},
|
|
||||||
]
|
|
||||||
regex = [
|
|
||||||
{file = "regex-2020.4.4-cp27-cp27m-win32.whl", hash = "sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f"},
|
|
||||||
{file = "regex-2020.4.4-cp27-cp27m-win_amd64.whl", hash = "sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1"},
|
|
||||||
{file = "regex-2020.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b"},
|
|
||||||
{file = "regex-2020.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db"},
|
|
||||||
{file = "regex-2020.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156"},
|
|
||||||
{file = "regex-2020.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3"},
|
|
||||||
{file = "regex-2020.4.4-cp36-cp36m-win32.whl", hash = "sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8"},
|
|
||||||
{file = "regex-2020.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a"},
|
|
||||||
{file = "regex-2020.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468"},
|
|
||||||
{file = "regex-2020.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6"},
|
|
||||||
{file = "regex-2020.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd"},
|
|
||||||
{file = "regex-2020.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948"},
|
|
||||||
{file = "regex-2020.4.4-cp37-cp37m-win32.whl", hash = "sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e"},
|
|
||||||
{file = "regex-2020.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a"},
|
|
||||||
{file = "regex-2020.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e"},
|
|
||||||
{file = "regex-2020.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683"},
|
|
||||||
{file = "regex-2020.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b"},
|
|
||||||
{file = "regex-2020.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"},
|
|
||||||
{file = "regex-2020.4.4-cp38-cp38-win32.whl", hash = "sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3"},
|
|
||||||
{file = "regex-2020.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3"},
|
|
||||||
{file = "regex-2020.4.4.tar.gz", hash = "sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142"},
|
|
||||||
]
|
|
||||||
requests = [
|
|
||||||
{file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
|
|
||||||
{file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
|
|
||||||
]
|
|
||||||
"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"},
|
|
||||||
]
|
|
||||||
toml = [
|
|
||||||
{file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
|
|
||||||
{file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"},
|
|
||||||
{file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"},
|
|
||||||
]
|
|
||||||
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"},
|
|
||||||
]
|
|
||||||
urllib3 = [
|
|
||||||
{file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"},
|
|
||||||
{file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"},
|
|
||||||
]
|
|
@ -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,27 +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"
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
|
||||||
black = "^19.10b0"
|
|
||||||
isort = "^4.3.21"
|
|
||||||
|
|
||||||
[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"
|
||||||
|
]
|
||||||
|
}
|
@ -7,4 +7,4 @@ User=adam
|
|||||||
Type=oneshot
|
Type=oneshot
|
||||||
TimeoutStartSec=600
|
TimeoutStartSec=600
|
||||||
WorkingDirectory=/home/adam/memberPlumbing/
|
WorkingDirectory=/home/adam/memberPlumbing/
|
||||||
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run /home/adam/memberPlumbing/bin/doorUpdater
|
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run doorUpdater
|
||||||
|
@ -9,4 +9,4 @@ User=adam
|
|||||||
Type=oneshot
|
Type=oneshot
|
||||||
TimeoutStartSec=600
|
TimeoutStartSec=600
|
||||||
WorkingDirectory=/home/adam/memberPlumbing/
|
WorkingDirectory=/home/adam/memberPlumbing/
|
||||||
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run /home/adam/memberPlumbing/bin/hidEvents
|
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run hidEvents
|
||||||
|
@ -9,4 +9,4 @@ User=adam
|
|||||||
Type=oneshot
|
Type=oneshot
|
||||||
TimeoutStartSec=600
|
TimeoutStartSec=600
|
||||||
WorkingDirectory=/home/adam/memberPlumbing/
|
WorkingDirectory=/home/adam/memberPlumbing/
|
||||||
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run /home/adam/memberPlumbing/bin/sqlExport
|
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run sqlExport
|
||||||
|
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