forked from CMS/memberPlumbing
Compare commits
64 Commits
auto-reboo
...
master
Author | SHA1 | Date | |
---|---|---|---|
66853e1156 | |||
363be0ba8c | |||
d8b3958c87 | |||
68b4b10c51 | |||
88b2610513 | |||
3f17cd9ec2 | |||
5c53f7f88c | |||
97c4dbc1ee | |||
3595a24d85 | |||
c1430e2f9a | |||
a5a787e0f7 | |||
6b7194c15a | |||
855f9b652d | |||
69bcb71091 | |||
63bd8efaf2 | |||
af6ecb2864 | |||
ce2a0f4c1d | |||
995b6f9763 | |||
f94a27699c | |||
34539eb630 | |||
3849aca918 | |||
cfccc433dd | |||
a2dd00f414 | |||
5a39c5cae9 | |||
7a22f43ccf | |||
2549bff31f | |||
51dd059892 | |||
ed1cbdcd2d | |||
9970838b5c | |||
85a2ab977a | |||
7ca2b30d4a | |||
2eacb46353 | |||
50952bdb46 | |||
af0584651c | |||
ca9a089108 | |||
2c3cf27779 | |||
e37770dbe2 | |||
a53ad5edd4 | |||
afd6ffbdc0 | |||
743fe3b9fb | |||
b509495c5f | |||
7981a05a46 | |||
525fd24a22 | |||
cf84086446 | |||
952852badb | |||
e832851fc4 | |||
a201b9f09c | |||
06516ad0cd | |||
f95493e3a6 | |||
641b9a2779 | |||
9d743344ab | |||
c52b76534c | |||
25532bf21b | |||
d5be64c37d | |||
d248e41fdb | |||
82a54b8f41 | |||
659459ddd3 | |||
2b894d3cc9 | |||
21a9aa5b5c | |||
8367c8bbc1 | |||
667260831c | |||
bb18f34b2e | |||
df92332c69 | |||
d867cacfef |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
/passwords.py
|
/venv/
|
||||||
|
/config.yaml
|
||||||
|
39
README.md
Normal file
39
README.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Claremont MakerSpace Member Plumbing
|
||||||
|
|
||||||
|
This repo contains a set of scripts to sync data around for the Claremont MakerSpace. They primarily revolve around pulling member data from [MembershipWorks](https://membershipworks.com/) and pushing it out to various systems at the Space.
|
||||||
|
|
||||||
|
## 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 <script>` to start a specific script.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
Many of the scripts use data from a `config.yaml` in the current working directory when they are run. There is an example config in [`config.example.yaml`](./config.example.yaml) which has been stripped of authentication information.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
Retrieves member information from MembershipWorks and pushes it out to the HID Edge Evo SOLO controllers that do access control at the Space. Configuration lives in `config.yaml`.
|
||||||
|
|
||||||
|
### `ucsAccounts`
|
||||||
|
|
||||||
|
Retrieves member information from MembershipWorks and pushes it out to [UCS](https://www.univention.com/products/ucs/), which we use as a domain controller for the Windows computers at the Space.
|
||||||
|
|
||||||
|
### `sqlExport`
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
Retrieves events from the HID Evo Solo door controllers, and pushes them to a SQL database.
|
||||||
|
|
||||||
|
## Systemd
|
||||||
|
|
||||||
|
There are systemd units in the [`systemd`](./systemd/) folder, which can be used to run the various scripts regularly.
|
81
common.py
81
common.py
@ -1,81 +0,0 @@
|
|||||||
import csv
|
|
||||||
import json
|
|
||||||
import urllib3
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from io import StringIO
|
|
||||||
import requests
|
|
||||||
from hid.DoorController import DoorController
|
|
||||||
|
|
||||||
from passwords import *
|
|
||||||
|
|
||||||
# it's fine, ssl certs are for losers anyway
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
||||||
|
|
||||||
try:
|
|
||||||
config = json.load(
|
|
||||||
open(os.path.dirname(os.path.abspath(__file__)) + "/config.json"))
|
|
||||||
except NameError:
|
|
||||||
config = json.load(open("config.json"))
|
|
||||||
|
|
||||||
doors = {doorName: DoorController(doorData['ip'],
|
|
||||||
DOOR_USERNAME, DOOR_PASSWORD,
|
|
||||||
name=doorName, access=doorData['access'])
|
|
||||||
for doorName, doorData in config["doors"].items()}
|
|
||||||
|
|
||||||
# mapping of member levels to schedules
|
|
||||||
memberLevels = {"CMS Staff": "7x24",
|
|
||||||
"CMS Weekends Only": "Weekends Only",
|
|
||||||
"CMS Weekdays Only": "Weekdays Only",
|
|
||||||
"CMS Unlimited": "Unlimited",
|
|
||||||
"CMS Nights & Weekends": "Nights and Weekends",
|
|
||||||
"CMS Day Pass": "Unlimited"}
|
|
||||||
|
|
||||||
def getMembershipworksData(folders, columns):
|
|
||||||
""" Pull the members csv from the membershipworks api
|
|
||||||
folders: a list of the names of the folders to get
|
|
||||||
(see folder_map in this function for mapping to ids)
|
|
||||||
columns: which columns to get"""
|
|
||||||
BASE_URL = "https://api.membershipworks.com/v1/"
|
|
||||||
folder_map = {'members': '5ae37979f033bfe8534f8799',
|
|
||||||
'staff': '5771675edcdf126302a2f6b9',
|
|
||||||
'misc': '5b69ee9bf033bf8e7346c434'}
|
|
||||||
|
|
||||||
# login
|
|
||||||
r = requests.post(BASE_URL + 'usr',
|
|
||||||
data={"_st": "all",
|
|
||||||
"eml": MEMBERSHIPWORKS_USERNAME,
|
|
||||||
"org": "10000",
|
|
||||||
"pwd": MEMBERSHIPWORKS_PASSWORD})
|
|
||||||
if r.status_code != 200 or 'SF' not in r.json():
|
|
||||||
print("MembershipWorks Login Error: ", r.status_code, r.reason)
|
|
||||||
print(r.text)
|
|
||||||
sys.exit(1)
|
|
||||||
login_data = r.json()
|
|
||||||
|
|
||||||
# get list of member/staff IDs
|
|
||||||
r = requests.get(BASE_URL + "ylp",
|
|
||||||
params={"SF": login_data['SF'],
|
|
||||||
"lbl": ",".join([folder_map[f] for f in folders]),
|
|
||||||
"org": login_data['org'],
|
|
||||||
"var": "_id,nam,ctc"})
|
|
||||||
if r.status_code != 200 or 'usr' not in r.json():
|
|
||||||
print("MembershipWorks User Listing Error: ", r.status_code, r.reason)
|
|
||||||
print(r.text)
|
|
||||||
sys.exit(1)
|
|
||||||
ids = [user['uid'] for user in r.json()['usr']]
|
|
||||||
|
|
||||||
# get members CSV
|
|
||||||
# TODO: maybe can just use previous get instead? would return JSON
|
|
||||||
r = requests.post(BASE_URL + "csv",
|
|
||||||
params={"SF": login_data['SF']},
|
|
||||||
data={"_rt": "946702800", # unknown
|
|
||||||
"mux": "", # unknown
|
|
||||||
"tid": ",".join(ids), # ids of members to get
|
|
||||||
"var": columns})
|
|
||||||
if r.status_code != 200:
|
|
||||||
print("MembershipWorks CSV Generation Error: ", r.status_code, r.reason)
|
|
||||||
print(r.text)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return list(csv.DictReader(StringIO(r.text)))
|
|
51
config.example.yaml
Normal file
51
config.example.yaml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
doorControllers:
|
||||||
|
Studio Space: {ip: 172.18.51.11, access: Studio Space}
|
||||||
|
Front Door: {ip: 172.18.51.12, access: Front Door}
|
||||||
|
Metal Shop: {ip: 172.18.51.13, access: Metal Shop}
|
||||||
|
Wood Shop: {ip: 172.18.51.14, access: Wood Shop}
|
||||||
|
Wood Shop Rear: {ip: 172.18.51.15, access: Wood Shop}
|
||||||
|
Storage Closet: {ip: 172.18.51.16, access: Storage Closet}
|
||||||
|
|
||||||
|
# {member type: door schedule}
|
||||||
|
memberLevels:
|
||||||
|
CMS Staff: 7x24
|
||||||
|
CMS Weekends Only: Weekends Only
|
||||||
|
CMS Weekdays Only: Weekdays Only
|
||||||
|
CMS Unlimited: Unlimited
|
||||||
|
CMS Nights & Weekends: Nights and Weekends
|
||||||
|
CMS Day Pass: Unlimited
|
||||||
|
|
||||||
|
# {schedule: {property: [doors]}}
|
||||||
|
doorSpecificSchedules:
|
||||||
|
Extended Hours:
|
||||||
|
Access Front Door and Studio Space During Extended Hours?:
|
||||||
|
- Front Door
|
||||||
|
- Studio Space
|
||||||
|
- Storage Closet
|
||||||
|
Access Permitted Shops During Extended Hours?:
|
||||||
|
- Metal Shop
|
||||||
|
- Wood Shop
|
||||||
|
- Wood Shop Rear
|
||||||
|
|
||||||
|
|
||||||
|
DOOR_USERNAME: ""
|
||||||
|
DOOR_PASSWORD: ""
|
||||||
|
|
||||||
|
MEMBERSHIPWORKS_USERNAME: ""
|
||||||
|
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:
|
||||||
|
database: ""
|
||||||
|
user: ""
|
||||||
|
password: ""
|
||||||
|
|
||||||
|
HID_DB:
|
||||||
|
database: ""
|
||||||
|
user: ""
|
||||||
|
password: ""
|
10
config.json
10
config.json
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"doors": {
|
|
||||||
"Studio Space": {"ip": "172.18.51.11", "access": "Studio Space"},
|
|
||||||
"Front Door": {"ip": "172.18.51.12", "access": "Front Door"},
|
|
||||||
"Metal Shop": {"ip": "172.18.51.13", "access": "Metal Shop"},
|
|
||||||
"Wood Shop": {"ip": "172.18.51.14", "access": "Wood Shop"},
|
|
||||||
"Wood Shop Rear": {"ip": "172.18.51.15", "access": "Wood Shop"},
|
|
||||||
"Storage Closet": {"ip": "172.18.51.16", "access": "Storage Closet"}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import sys
|
|
||||||
import requests
|
|
||||||
import csv
|
|
||||||
from collections import OrderedDict
|
|
||||||
from io import StringIO
|
|
||||||
from hashlib import md5
|
|
||||||
import os
|
|
||||||
|
|
||||||
from common import *
|
|
||||||
from hid.DoorController import fieldnames
|
|
||||||
|
|
||||||
def makeMember(member, doorAuth):
|
|
||||||
"""Create an output CSV row for the member"""
|
|
||||||
if member["Access Card Number"] == "":
|
|
||||||
#print(member["First Name"], member["Last Name"], " has no card number, ignoring")
|
|
||||||
return
|
|
||||||
|
|
||||||
out = {"Forename": member["First Name"],
|
|
||||||
"Surname": member["Last Name"],
|
|
||||||
"Initial": "",
|
|
||||||
"CardNumber": member["Access Card Number"],
|
|
||||||
"CardFormat": "A901146A-" + member["Access Card Facility Code"],
|
|
||||||
"PinRequired": "0",
|
|
||||||
"ExtendedAccess": "0",
|
|
||||||
"ExpiryDate": "",
|
|
||||||
"Email": member["Email"],
|
|
||||||
"Phone": member["Phone"]}
|
|
||||||
|
|
||||||
if member[doorAuth] == "Y" \
|
|
||||||
and not member["Account on Hold"] == "Account on Hold":
|
|
||||||
levels = OrderedDict(sorted([(k, v) for k, v in memberLevels.items() if member[k] == k]))
|
|
||||||
out["Custom1"] = "|".join(levels.keys()).replace("&", "and")
|
|
||||||
for index, schedule in enumerate(levels.values(), 1):
|
|
||||||
#TODO: error if people have more than 8?
|
|
||||||
out["Schedule" + str(index)] = schedule
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
def makeDoor(door, members, hashes):
|
|
||||||
"""Create a CSV for the given door"""
|
|
||||||
outString = StringIO()
|
|
||||||
writer = csv.DictWriter(outString, fieldnames)
|
|
||||||
writer.writeheader()
|
|
||||||
for member in members:
|
|
||||||
member = makeMember(member, "Access " + door.access + "?")
|
|
||||||
if member is not None:
|
|
||||||
writer.writerow(member)
|
|
||||||
|
|
||||||
import datetime as DT
|
|
||||||
timestamp = DT.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
with open("/tmp/" + door.name + timestamp + ".csv", "w") as f:
|
|
||||||
f.write(outString.getvalue())
|
|
||||||
|
|
||||||
outString.seek(0)
|
|
||||||
doorHash = md5(bytes(outString.getvalue(), 'utf8')).hexdigest()
|
|
||||||
if doorHash == hashes.get(door.name):
|
|
||||||
print("Door", door.name, "not changed, not updating")
|
|
||||||
else:
|
|
||||||
print("Door", door.name, "changed, trying to update")
|
|
||||||
hashes[door.name] = doorHash
|
|
||||||
door.doCSVImport(outString)
|
|
||||||
# write out hash if we sucessfully updated this door
|
|
||||||
with open('/tmp/doorUpdaterLastHash', 'w') as f:
|
|
||||||
json.dump(hashes, f)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
members = getMembershipworksData(
|
|
||||||
['members', 'staff', 'misc'],
|
|
||||||
"lvl,xws,xms,xsc,xas,xfd,xac,phn,eml,lbl,xcf,nam")
|
|
||||||
members.sort(key=lambda x: x['Last Name'])
|
|
||||||
|
|
||||||
if os.path.exists('/tmp/doorUpdaterLastHash'):
|
|
||||||
with open('/tmp/doorUpdaterLastHash', 'r') as f:
|
|
||||||
hashes = json.load(f)
|
|
||||||
else:
|
|
||||||
hashes = {}
|
|
||||||
|
|
||||||
for door in doors.values():
|
|
||||||
print(door.name, door.ip)
|
|
||||||
makeDoor(door, members, hashes)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
25
doorUtil.py
25
doorUtil.py
@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import requests
|
|
||||||
import csv
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
from common import *
|
|
||||||
|
|
||||||
def hexToCode(hex):
|
|
||||||
b = bin(int(hex, 16))[2:]
|
|
||||||
facility = int(b[0:8], 2)
|
|
||||||
code = int(b[9:24], 2)
|
|
||||||
return((facility, code))
|
|
||||||
|
|
||||||
def codeToHex(facility, code):
|
|
||||||
return "{:08X}".format(int(bin(facility)[2:] + "0" + bin(code)[2:] + "1", 2))
|
|
||||||
|
|
||||||
# hexToCode("01E29DA1") <-> codeToHex(241, 20176)
|
|
||||||
|
|
||||||
def forEachDoor(fxn):
|
|
||||||
for door in doors.values():
|
|
||||||
print(door.name)
|
|
||||||
fxn(door)
|
|
||||||
|
|
||||||
#forEachDoor(lambda door: door.sendCardFormat("A901146A-244", 1, 244))
|
|
||||||
#forEachDoor(lambda door: door.sendSchedules())
|
|
67
events.py
67
events.py
@ -1,67 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
from collections import defaultdict
|
|
||||||
from lxml import etree
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from common import *
|
|
||||||
|
|
||||||
def getStrings(door):
|
|
||||||
"""Parses out the message strings from source."""
|
|
||||||
r = requests.get('https://' + door.ip + '/html/en_EN/en_EN.js',
|
|
||||||
auth=requests.auth.HTTPDigestAuth(door.username, door.password),
|
|
||||||
verify=False)
|
|
||||||
regex = re.compile(r'([0-9]+)="([^"]*)')
|
|
||||||
strings = [regex.search(s) for s in r.text.split(';')
|
|
||||||
if s.startswith('localeStrings.eventDetails')]
|
|
||||||
print({int(g.group(1)): g.group(2) for g in strings})
|
|
||||||
|
|
||||||
def getMessages(door):
|
|
||||||
# get parameters for messages to get?
|
|
||||||
# honestly not really sure why this is required, their API is confusing
|
|
||||||
parXMLIn = E_plain.VertXMessage(
|
|
||||||
E.EventMessages({"action": "LR"}))
|
|
||||||
parXMLOut = door.doXMLRequest(parXMLIn)
|
|
||||||
etree.dump(parXMLOut)
|
|
||||||
|
|
||||||
if os.path.exists("logs/" + door.name + ".xml"):
|
|
||||||
# read last log
|
|
||||||
tree = etree.ElementTree(file="logs/" + door.name + ".xml")
|
|
||||||
root = tree.getroot()
|
|
||||||
recordCount = int(parXMLOut[0].attrib["historyRecordMarker"]) - \
|
|
||||||
int(root[0][0].attrib["recordMarker"])
|
|
||||||
else:
|
|
||||||
# first run for this door
|
|
||||||
root = None
|
|
||||||
recordCount = 1000
|
|
||||||
|
|
||||||
if recordCount == 0:
|
|
||||||
print("No records to get!")
|
|
||||||
return
|
|
||||||
print("Getting", recordCount, "records")
|
|
||||||
# get the actual messages
|
|
||||||
eventsXMLIn = E_plain.VertXMessage(
|
|
||||||
E.EventMessages({"action": "LR",
|
|
||||||
"recordCount": str(recordCount),
|
|
||||||
"historyRecordMarker": parXMLOut[0].attrib["historyRecordMarker"],
|
|
||||||
"historyTimestamp": parXMLOut[0].attrib["historyTimestamp"]}))
|
|
||||||
eventsXMLOut = door.doXMLRequest(eventsXMLIn)
|
|
||||||
#TODO: handle modeRecords=true
|
|
||||||
|
|
||||||
for index, event in enumerate(eventsXMLOut[0]):
|
|
||||||
event.attrib["recordMarker"] = str(int(parXMLOut[0].attrib["historyRecordMarker"]) - index)
|
|
||||||
|
|
||||||
if root is None:
|
|
||||||
tree = etree.ElementTree(eventsXMLOut)
|
|
||||||
else:
|
|
||||||
for event in reversed(eventsXMLOut[0]):
|
|
||||||
root[0].insert(0, event)
|
|
||||||
tree.write("logs/" + doorName + ".xml")
|
|
||||||
|
|
||||||
def main():
|
|
||||||
for door in doors.values():
|
|
||||||
getMessages(door)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,122 +0,0 @@
|
|||||||
import csv
|
|
||||||
from io import StringIO
|
|
||||||
from lxml import etree
|
|
||||||
from lxml.builder import ElementMaker
|
|
||||||
import requests
|
|
||||||
|
|
||||||
E_plain = ElementMaker(nsmap={"hid": "http://www.hidglobal.com/VertX"})
|
|
||||||
E = ElementMaker(namespace="http://www.hidglobal.com/VertX",
|
|
||||||
nsmap={"hid": "http://www.hidglobal.com/VertX"})
|
|
||||||
E_corp = ElementMaker(namespace="http://www.hidcorp.com/VertX", #stupid
|
|
||||||
nsmap={"hid": "http://www.hidcorp.com/VertX"})
|
|
||||||
ROOT = E_plain.VertXMessage
|
|
||||||
|
|
||||||
fieldnames = "CardNumber,CardFormat,PinRequired,PinCode,ExtendedAccess,ExpiryDate,Forename,Initial,Surname,Email,Phone,Custom1,Custom2,Schedule1,Schedule2,Schedule3,Schedule4,Schedule5,Schedule6,Schedule7,Schedule8".split(",")
|
|
||||||
|
|
||||||
class RemoteError(Exception):
|
|
||||||
def __init__(self, r):
|
|
||||||
super().__init__(
|
|
||||||
"Door Updating Error: {} {}\n{}"
|
|
||||||
.format(r.status_code, r.reason, r.text))
|
|
||||||
|
|
||||||
class DoorController():
|
|
||||||
def __init__(self, ip, username, password, name="", access=""):
|
|
||||||
self.ip = ip
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
self.name = name
|
|
||||||
self.access = access
|
|
||||||
|
|
||||||
def doImportRequest(self, params=None, files=None):
|
|
||||||
"""Send a request to the door control import script"""
|
|
||||||
r = requests.post(
|
|
||||||
'https://' + self.ip + '/cgi-bin/import.cgi',
|
|
||||||
params=params,
|
|
||||||
files=files,
|
|
||||||
auth=requests.auth.HTTPDigestAuth(self.username, self.password),
|
|
||||||
timeout=60,
|
|
||||||
verify=False) # ignore insecure SSL
|
|
||||||
xml = etree.XML(r.content)
|
|
||||||
if r.status_code != 200 \
|
|
||||||
or len(xml.findall("{http://www.hidglobal.com/VertX}Error")) > 0:
|
|
||||||
raise RemoteError(r)
|
|
||||||
|
|
||||||
def doCSVImport(self, csv):
|
|
||||||
"""Do the CSV import procedure on a door control"""
|
|
||||||
self.doImportRequest({"task": "importInit"})
|
|
||||||
self.doImportRequest({"task": "importCardsPeople", "name": "cardspeopleschedule.csv"},
|
|
||||||
{"importCardsPeopleButton": ("cardspeopleschedule.csv", csv, 'text/csv')})
|
|
||||||
self.doImportRequest({"task": "importDone"})
|
|
||||||
|
|
||||||
def doXMLRequest(self, xml, prefix=b'<?xml version="1.0" encoding="UTF-8"?>'):
|
|
||||||
if not isinstance(xml, str): xml = etree.tostring(xml)
|
|
||||||
r = requests.get(
|
|
||||||
'https://' + self.ip + '/cgi-bin/vertx_xml.cgi',
|
|
||||||
params={'XML': prefix + xml},
|
|
||||||
auth=requests.auth.HTTPDigestAuth(self.username, self.password),
|
|
||||||
verify=False)
|
|
||||||
resp_xml = etree.XML(r.content)
|
|
||||||
# probably meed to be more sane about this
|
|
||||||
if r.status_code != 200 \
|
|
||||||
or len(resp_xml.findall("{*}Error")) > 0:
|
|
||||||
raise RemoteError(r)
|
|
||||||
return resp_xml
|
|
||||||
|
|
||||||
def sendSchedules(self, schedules):
|
|
||||||
# clear all people
|
|
||||||
outString = StringIO()
|
|
||||||
writer = csv.DictWriter(outString, fieldnames)
|
|
||||||
writer.writeheader()
|
|
||||||
writer.writerow({})
|
|
||||||
outString.seek(0)
|
|
||||||
self.doCSVImport(outString)
|
|
||||||
|
|
||||||
# clear all schedules
|
|
||||||
delXML = ROOT(
|
|
||||||
*[E.Schedules({"action": "DD", "scheduleID": str(ii)})
|
|
||||||
for ii in range(1, 8)])
|
|
||||||
try:
|
|
||||||
self.doXMLRequest(delXML)
|
|
||||||
except RemoteError:
|
|
||||||
# don't care about failure to delete, they probably just didn't exist
|
|
||||||
pass
|
|
||||||
|
|
||||||
# load new schedules
|
|
||||||
self.doXMLRequest(schedules)
|
|
||||||
|
|
||||||
def getSchedules(self):
|
|
||||||
# TODO: might be able to do in one request
|
|
||||||
schedules = self.doXMLRequest(ROOT(
|
|
||||||
E.Schedules({"action": "LR"})))
|
|
||||||
etree.dump(schedules)
|
|
||||||
|
|
||||||
data = self.doXMLRequest(ROOT(
|
|
||||||
*[E.Schedules({"action": "LR",
|
|
||||||
"scheduleID": schedule.attrib["scheduleID"]})
|
|
||||||
for schedule in schedules[0]]))
|
|
||||||
return ROOT(E_corp.Schedules({"action": "AD"},
|
|
||||||
*[s[0] for s in data]))
|
|
||||||
|
|
||||||
def sendCardFormat(self, formatName, templateID, facilityCode):
|
|
||||||
# TODO: add ability to delete formats
|
|
||||||
# delete example: <hid:CardFormats action="DD" formatID="7-1-244"/>
|
|
||||||
|
|
||||||
el = ROOT(
|
|
||||||
E.CardFormats({"action": "AD"},
|
|
||||||
E.CardFormat({"formatName": formatName,
|
|
||||||
"templateID": str(templateID)},
|
|
||||||
E.FixedField({"value": str(facilityCode)}))))
|
|
||||||
return self.doXMLRequest(el)
|
|
||||||
|
|
||||||
def lockOrUnlockDoor(self, lock=True):
|
|
||||||
el = ROOT(
|
|
||||||
E.Doors({"action": "CM",
|
|
||||||
"command": "lockDoor" if lock else "unlockDoor"}))
|
|
||||||
return self.doXMLRequest(el)
|
|
||||||
|
|
||||||
def getStatus(self):
|
|
||||||
el = ROOT(
|
|
||||||
E.Doors({"action": "LR", "responseFormat": "status"}))
|
|
||||||
xml = self.doXMLRequest(el)
|
|
||||||
relayState = xml.find('./{*}Doors/{*}Door').attrib['relayState']
|
|
||||||
return "unlocked" if relayState == "set" else "locked"
|
|
265
memberPlumbing/MembershipWorks.py
Normal file
265
memberPlumbing/MembershipWorks.py
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import csv
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
BASE_URL = "https://api.membershipworks.com"
|
||||||
|
|
||||||
|
# extracted from `SF._is.crm` in https://cdn.membershipworks.com/all.js
|
||||||
|
CRM = {
|
||||||
|
0: "Note",
|
||||||
|
4: "Profile Updated",
|
||||||
|
8: "Scheduled/Reminder Email",
|
||||||
|
9: "Renewal Notice",
|
||||||
|
10: "Join Date",
|
||||||
|
11: "Next Renewal Date",
|
||||||
|
12: "Membership Payment",
|
||||||
|
13: "Donation",
|
||||||
|
14: "Event Activity",
|
||||||
|
15: "Conversation",
|
||||||
|
16: "Contact Change",
|
||||||
|
17: "Label Change",
|
||||||
|
18: "Other Payment",
|
||||||
|
19: "Cart Payment",
|
||||||
|
20: "Payment Failed",
|
||||||
|
21: "Billing Updated",
|
||||||
|
22: "Form Checkout",
|
||||||
|
23: "Event Payment",
|
||||||
|
24: "Invoice",
|
||||||
|
25: "Invoice Payment",
|
||||||
|
26: "Renewal",
|
||||||
|
27: "Payment",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Types of fields, extracted from a html snippet in all.js + some guessing
|
||||||
|
typ = {
|
||||||
|
1: "Text input",
|
||||||
|
2: "Password", # inferred from data
|
||||||
|
3: "Simple text area",
|
||||||
|
4: "Rich text area",
|
||||||
|
7: "Address",
|
||||||
|
8: "Check box",
|
||||||
|
9: "Select",
|
||||||
|
11: "Display value stored in field (ie. read only)",
|
||||||
|
12: "Required waiver/terms",
|
||||||
|
}
|
||||||
|
|
||||||
|
# more constants, this time extracted from the members csv export in all.js
|
||||||
|
staticFlags = {
|
||||||
|
"pos": {"lbl": "Position/relation (contacts)"},
|
||||||
|
"nte": {"lbl": "Admin note (contacts)"},
|
||||||
|
"pwd": {"lbl": "Password"},
|
||||||
|
"lgo": {"lbl": "Business card image URLs"},
|
||||||
|
"pfx": {"lbl": "Profile gallery image URLs"},
|
||||||
|
"lvl": {"lbl": "Membership levels"},
|
||||||
|
"aon": {"lbl": "Membership add-ons"},
|
||||||
|
"lbl": {"lbl": "Labels"},
|
||||||
|
"joi": {"lbl": "Join date"},
|
||||||
|
"end": {"lbl": "Renewal date"},
|
||||||
|
"spy": {"lbl": "Billing method"},
|
||||||
|
"rid": {"lbl": "Auto recurring billing ID"},
|
||||||
|
"ipa": {"lbl": "IP address"},
|
||||||
|
"_id": {"lbl": "Account ID"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipWorksRemoteError(Exception):
|
||||||
|
def __init__(self, reason, r):
|
||||||
|
super().__init__(
|
||||||
|
f"Error when attempting {reason}: {r.status_code} {r.reason}\n{r.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipWorks:
|
||||||
|
def __init__(self):
|
||||||
|
self.sess = requests.Session()
|
||||||
|
self.org_info = None
|
||||||
|
self.auth_token = None
|
||||||
|
self.org_num = None
|
||||||
|
|
||||||
|
def login(self, username, password):
|
||||||
|
"""Authenticate against the membershipworks api"""
|
||||||
|
r = self.sess.post(
|
||||||
|
BASE_URL + "/v2/account/session",
|
||||||
|
data={"eml": username, "pwd": password},
|
||||||
|
headers={"X-Org": "10000"},
|
||||||
|
)
|
||||||
|
if r.status_code != 200 or "SF" not in r.json():
|
||||||
|
raise MembershipWorksRemoteError("login", r)
|
||||||
|
self.org_info = r.json()
|
||||||
|
self.auth_token = self.org_info["SF"]
|
||||||
|
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):
|
||||||
|
# TODO: should probably be a decorator or something
|
||||||
|
if self.auth_token is None:
|
||||||
|
raise RuntimeError("Not Logged in to MembershipWorks")
|
||||||
|
# add auth token to params
|
||||||
|
if "params" not in kwargs:
|
||||||
|
kwargs["params"] = {}
|
||||||
|
kwargs["params"]["SF"] = self.auth_token
|
||||||
|
|
||||||
|
def _get_v1(self, *args, **kwargs):
|
||||||
|
self._inject_auth(kwargs)
|
||||||
|
# TODO: should probably do some error handling in here
|
||||||
|
return requests.get(*args, **kwargs)
|
||||||
|
|
||||||
|
def _post_v1(self, *args, **kwargs):
|
||||||
|
self._inject_auth(kwargs)
|
||||||
|
# TODO: should probably do some error handling in here
|
||||||
|
return requests.post(*args, **kwargs)
|
||||||
|
|
||||||
|
def _all_fields(self):
|
||||||
|
"""Parse out a list of fields from the org data.
|
||||||
|
|
||||||
|
Is this terrible? Yes. Also, not dissimilar to how MW does it
|
||||||
|
in all.js.
|
||||||
|
"""
|
||||||
|
fields = staticFlags.copy()
|
||||||
|
|
||||||
|
# TODO: this will take the later option, if the same field
|
||||||
|
# used in mulitple places. I don't know which lbl is used for
|
||||||
|
# csv export
|
||||||
|
|
||||||
|
# anm: member signup, acc: member manage, adm: admin manage
|
||||||
|
for screen_type in ["anm", "acc", "adm"]:
|
||||||
|
for box in self.org_info["tpl"][screen_type]:
|
||||||
|
for element in box["box"]:
|
||||||
|
if type(element["dat"]) != str:
|
||||||
|
for field in element["dat"]:
|
||||||
|
if "_id" in field:
|
||||||
|
if field["_id"] not in fields:
|
||||||
|
fields[field["_id"]] = field
|
||||||
|
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def _parse_flags(self):
|
||||||
|
"""Parse the flags out of the org data.
|
||||||
|
|
||||||
|
This is terrible, and there might be a better way to do this.
|
||||||
|
"""
|
||||||
|
ret = {"folders": {}, "levels": {}, "addons": {}, "labels": {}}
|
||||||
|
|
||||||
|
for dek in self.org_info["dek"]:
|
||||||
|
# TODO: there must be a better way. this is stupid
|
||||||
|
if dek["dek"] == 1:
|
||||||
|
ret["folders"][dek["lbl"]] = dek["did"]
|
||||||
|
elif "cur" in dek:
|
||||||
|
ret["levels"][dek["lbl"]] = dek["did"]
|
||||||
|
elif "mux" in dek:
|
||||||
|
ret["addons"][dek["lbl"]] = dek["did"]
|
||||||
|
else:
|
||||||
|
ret["labels"][dek["lbl"]] = dek["did"]
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def get_member_ids(self, folders):
|
||||||
|
folder_map = self._parse_flags()["folders"]
|
||||||
|
|
||||||
|
r = self.sess.get(
|
||||||
|
BASE_URL + "/v2/accounts",
|
||||||
|
params={"dek": ",".join([folder_map[f] for f in folders])},
|
||||||
|
)
|
||||||
|
if r.status_code != 200 or "usr" not in r.json():
|
||||||
|
raise MembershipWorksRemoteError("user listing", r)
|
||||||
|
|
||||||
|
# get list of member ID matching the search
|
||||||
|
# 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:
|
||||||
|
# ex: "Personal Studio Space" Label vs Membership Addon/Field
|
||||||
|
def get_members(self, folders, columns):
|
||||||
|
"""Pull the members csv from the membershipworks api
|
||||||
|
folders: a list of the names of the folders to get
|
||||||
|
(see folder_map in this function for mapping to ids)
|
||||||
|
columns: which columns to get"""
|
||||||
|
ids = self.get_member_ids(folders)
|
||||||
|
|
||||||
|
# get members CSV
|
||||||
|
# TODO: maybe can just use previous get instead? would return JSON
|
||||||
|
r = self._post_v1(
|
||||||
|
BASE_URL + "/v1/csv",
|
||||||
|
data={
|
||||||
|
"_rt": "946702800", # unknown
|
||||||
|
"mux": "", # unknown
|
||||||
|
"tid": ",".join(ids), # ids of members to get
|
||||||
|
"var": columns,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise MembershipWorksRemoteError("csv generation", r)
|
||||||
|
|
||||||
|
if r.text[0] == "\ufeff":
|
||||||
|
r.encoding = r.encoding + "-sig"
|
||||||
|
|
||||||
|
return list(csv.DictReader(StringIO(r.text)))
|
||||||
|
|
||||||
|
def get_transactions(self, start_date, end_date, json=False):
|
||||||
|
"""Get the transactions between start_date and end_date
|
||||||
|
|
||||||
|
Dates can be datetime.date or datetime.datetime
|
||||||
|
|
||||||
|
json gets a different version of the transactions list,
|
||||||
|
which contains a different set information
|
||||||
|
"""
|
||||||
|
r = self._get_v1(
|
||||||
|
BASE_URL + "/v1/csv",
|
||||||
|
params={
|
||||||
|
"crm": ",".join(str(k) for k in CRM.keys()),
|
||||||
|
**({"txl": ""} if json else {}),
|
||||||
|
"sdp": start_date.strftime("%s"),
|
||||||
|
"edp": end_date.strftime("%s"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise MembershipWorksRemoteError("csv generation", r)
|
||||||
|
if json:
|
||||||
|
return r.json()
|
||||||
|
else:
|
||||||
|
if r.text[0] == "\ufeff":
|
||||||
|
r.encoding = r.encoding + "-sig"
|
||||||
|
|
||||||
|
return list(csv.DictReader(StringIO(r.text)))
|
||||||
|
|
||||||
|
def get_all_members(self):
|
||||||
|
"""Get all the data for all the members"""
|
||||||
|
folders = self._parse_flags()["folders"].keys()
|
||||||
|
fields = self._all_fields()
|
||||||
|
members = self.get_members(folders, ",".join(fields.keys()))
|
||||||
|
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()
|
0
memberPlumbing/__init__.py
Normal file
0
memberPlumbing/__init__.py
Normal file
36
memberPlumbing/config.py
Normal file
36
memberPlumbing/config.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
from .hid.DoorController import DoorController
|
||||||
|
from .MembershipWorks import MembershipWorks
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self, path="config.yaml"):
|
||||||
|
with open(path) as f:
|
||||||
|
self._data = YAML().load(f)
|
||||||
|
self.__dict__.update(self._data)
|
||||||
|
|
||||||
|
# lazy init, because this actually talks to an external server
|
||||||
|
self._membershipworks = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def doors(self):
|
||||||
|
return {
|
||||||
|
doorName: DoorController(
|
||||||
|
doorData["ip"],
|
||||||
|
self.DOOR_USERNAME,
|
||||||
|
self.DOOR_PASSWORD,
|
||||||
|
name=doorName,
|
||||||
|
access=doorData["access"],
|
||||||
|
)
|
||||||
|
for doorName, doorData in self.doorControllers.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def membershipworks(self):
|
||||||
|
if not self._membershipworks:
|
||||||
|
self._membershipworks = MembershipWorks()
|
||||||
|
self._membershipworks.login(
|
||||||
|
self.MEMBERSHIPWORKS_USERNAME, self.MEMBERSHIPWORKS_PASSWORD
|
||||||
|
)
|
||||||
|
return self._membershipworks
|
370
memberPlumbing/doorUpdater.py
Executable file
370
memberPlumbing/doorUpdater.py
Executable file
@ -0,0 +1,370 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .hid.Credential import Credential
|
||||||
|
from .hid.DoorController import ROOT, E
|
||||||
|
|
||||||
|
|
||||||
|
class Member:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
forename="",
|
||||||
|
surname="",
|
||||||
|
membershipWorksID="",
|
||||||
|
middleName="",
|
||||||
|
email="",
|
||||||
|
phone="",
|
||||||
|
cardholderID=None,
|
||||||
|
doorAccess=[],
|
||||||
|
credentials=set(),
|
||||||
|
levels=[],
|
||||||
|
extraLevels=[],
|
||||||
|
schedules=[],
|
||||||
|
):
|
||||||
|
self.forename = forename
|
||||||
|
self.surname = surname
|
||||||
|
self.membershipWorksID = membershipWorksID
|
||||||
|
self.middleName = middleName
|
||||||
|
self.email = email
|
||||||
|
self.phone = phone
|
||||||
|
self.cardholderID = cardholderID
|
||||||
|
self.doorAccess = doorAccess
|
||||||
|
self.credentials = credentials
|
||||||
|
self.levels = levels
|
||||||
|
self.schedules = schedules
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"""Name: {self.forename} | {self.middleName} | {self.surname}
|
||||||
|
MembershipWorks ID: {self.membershipWorksID}
|
||||||
|
Email: {self.email}
|
||||||
|
Phone: {self.phone}
|
||||||
|
Cardholder ID: {self.cardholderID}
|
||||||
|
doorAccess: {self.doorAccess}
|
||||||
|
Credentials: {self.credentials}
|
||||||
|
Levels: {self.levels}
|
||||||
|
Schedules: {self.schedules}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipworksMember(Member):
|
||||||
|
def __init__(self, config, data, formerMember=False):
|
||||||
|
super().__init__(
|
||||||
|
data["First Name"],
|
||||||
|
data["Last Name"],
|
||||||
|
membershipWorksID=data["Account ID"],
|
||||||
|
email=data["Email"],
|
||||||
|
phone=data["Phone"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if data["Access Card Number"] != "":
|
||||||
|
self.credentials = set(
|
||||||
|
[
|
||||||
|
Credential(
|
||||||
|
code=(
|
||||||
|
data["Access Card Facility Code"],
|
||||||
|
data["Access Card Number"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.credentials = set()
|
||||||
|
|
||||||
|
self.onHold = (
|
||||||
|
data["Account on Hold"] != ""
|
||||||
|
or data["CMS Membership on hold"] == "CMS Membership on hold"
|
||||||
|
)
|
||||||
|
self.formerMember = formerMember
|
||||||
|
|
||||||
|
levels = {k: v for k, v in config.memberLevels.items() if data[k] == k}
|
||||||
|
self.levels = list(levels.keys())
|
||||||
|
self.schedules = list(levels.values())
|
||||||
|
|
||||||
|
self.extraLevels = {
|
||||||
|
schedule: sum(
|
||||||
|
(doors for prop, doors in props.items() if data[prop] == "Y"), []
|
||||||
|
)
|
||||||
|
for schedule, props in config.doorSpecificSchedules.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.doorAccess = [
|
||||||
|
door
|
||||||
|
for door, doorData in config.doors.items()
|
||||||
|
if data["Access " + doorData.access + "?"] == "Y"
|
||||||
|
]
|
||||||
|
|
||||||
|
def to_DoorMember(self, door):
|
||||||
|
doorLevels = [k for k, v in self.extraLevels.items() if door.name in v]
|
||||||
|
|
||||||
|
schedules = []
|
||||||
|
if door.name in self.doorAccess and not self.onHold and not self.formerMember:
|
||||||
|
schedules = self.schedules + doorLevels
|
||||||
|
|
||||||
|
dm = DoorMember(
|
||||||
|
door,
|
||||||
|
forename=self.forename,
|
||||||
|
surname=self.surname,
|
||||||
|
membershipWorksID=self.membershipWorksID,
|
||||||
|
email=self.email,
|
||||||
|
phone=self.phone,
|
||||||
|
levels=self.levels + doorLevels,
|
||||||
|
doorAccess=self.doorAccess,
|
||||||
|
credentials=self.credentials,
|
||||||
|
schedules=schedules,
|
||||||
|
)
|
||||||
|
|
||||||
|
return dm
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return (
|
||||||
|
super().__str__()
|
||||||
|
+ f"""OnHold? {self.onHold}
|
||||||
|
Former Member? {self.formerMember}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DoorMember(Member):
|
||||||
|
def __init__(self, door, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.door = door
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_cardholder(cls, data, door):
|
||||||
|
ch = cls(
|
||||||
|
door=door,
|
||||||
|
forename=data.get("forename", ""),
|
||||||
|
surname=data.get("surname", ""),
|
||||||
|
membershipWorksID=data.attrib.get("custom2", ""),
|
||||||
|
middleName=data.attrib.get("middleName", ""),
|
||||||
|
email=data.attrib.get("email", ""),
|
||||||
|
phone=data.attrib.get("phone", ""),
|
||||||
|
cardholderID=data.attrib["cardholderID"],
|
||||||
|
)
|
||||||
|
|
||||||
|
ch.credentials = set(
|
||||||
|
Credential(hex=(c.attrib["rawCardNumber"]))
|
||||||
|
for c in data.findall("{*}Credential")
|
||||||
|
)
|
||||||
|
|
||||||
|
ch.levels = data.attrib.get("custom1", "").split("|")
|
||||||
|
ch.schedules = [r.attrib["scheduleName"] for r in data.findall("{*}Role")]
|
||||||
|
|
||||||
|
return ch
|
||||||
|
|
||||||
|
def attribs(self):
|
||||||
|
return {
|
||||||
|
"forename": self.forename,
|
||||||
|
"surname": self.surname,
|
||||||
|
"middleName": self.middleName,
|
||||||
|
"email": self.email,
|
||||||
|
"phone": self.phone,
|
||||||
|
"custom1": "|".join(self.levels).replace("&", "and"),
|
||||||
|
"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):
|
||||||
|
cardFormats = door.get_cardFormats()
|
||||||
|
cardholders = {
|
||||||
|
member.membershipWorksID: member
|
||||||
|
for member in [
|
||||||
|
DoorMember.from_cardholder(ch, door) for ch in door.get_cardholders()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
schedulesMap = door.get_scheduleMap()
|
||||||
|
allCredentials = set(
|
||||||
|
Credential(hex=c.attrib["rawCardNumber"]) for c in door.get_credentials()
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: can I combine requests?
|
||||||
|
for membershipworksMember in members:
|
||||||
|
member = membershipworksMember.to_DoorMember(door)
|
||||||
|
# cardholder did not exist, so add them
|
||||||
|
if member.membershipWorksID not in cardholders:
|
||||||
|
print("- Adding Member {member.forename} {member.surname}:")
|
||||||
|
print(f" - {member.attribs()}")
|
||||||
|
resp = door.doXMLRequest(
|
||||||
|
ROOT(E.Cardholders({"action": "AD"}, E.Cardholder(member.attribs())))
|
||||||
|
)
|
||||||
|
member.cardholderID = resp.find("{*}Cardholders/{*}Cardholder").attrib[
|
||||||
|
"cardholderID"
|
||||||
|
]
|
||||||
|
|
||||||
|
# create a dummy ch to force an update
|
||||||
|
# TODO: probably a cleaner way to do this
|
||||||
|
ch = copy.copy(member)
|
||||||
|
ch.schedules = []
|
||||||
|
ch.credentials = set()
|
||||||
|
# cardholder exists, compare contents
|
||||||
|
else:
|
||||||
|
ch = cardholders.pop(member.membershipWorksID)
|
||||||
|
member.cardholderID = ch.cardholderID
|
||||||
|
|
||||||
|
if member.attribs() != ch.attribs(): # update cardholder attributes
|
||||||
|
print(f"- Updating profile for {member.forename} {member.surname}")
|
||||||
|
print(f" - Old: {ch.attribs()}")
|
||||||
|
print(f" - New: {member.attribs()}")
|
||||||
|
door.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
E.Cardholders(
|
||||||
|
{"action": "UD", "cardholderID": member.cardholderID},
|
||||||
|
E.CardHolder(member.attribs()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if member.credentials != ch.credentials:
|
||||||
|
print(f"- Updating card for {member.forename} {member.surname}")
|
||||||
|
print(f" - {ch.credentials} -> {member.credentials}")
|
||||||
|
|
||||||
|
oldCards = ch.credentials
|
||||||
|
newCards = member.credentials
|
||||||
|
|
||||||
|
allNewCards = set(
|
||||||
|
card
|
||||||
|
for m in members
|
||||||
|
if m != membershipworksMember
|
||||||
|
for card in m.credentials
|
||||||
|
)
|
||||||
|
|
||||||
|
# cards removed, and won't be reassigned to someone else
|
||||||
|
for card in (oldCards - newCards) - allNewCards:
|
||||||
|
door.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
E.Credentials(
|
||||||
|
{
|
||||||
|
"action": "UD",
|
||||||
|
"rawCardNumber": card.hex,
|
||||||
|
"isCard": "true",
|
||||||
|
},
|
||||||
|
E.Credential({"cardholderID": ""}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if newCards - oldCards: # cards added
|
||||||
|
for card in newCards & allNewCards: # new card exists in another member
|
||||||
|
print(
|
||||||
|
[
|
||||||
|
m
|
||||||
|
for m in members
|
||||||
|
for card in m.credentials
|
||||||
|
if card in newCards
|
||||||
|
]
|
||||||
|
)
|
||||||
|
raise Exception(f"Duplicate Card in input data! {card}")
|
||||||
|
|
||||||
|
# card existed in door, and needs to be reassigned
|
||||||
|
for card in newCards & allCredentials:
|
||||||
|
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
|
||||||
|
if newCards - allCredentials:
|
||||||
|
door.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
member.make_credentials(
|
||||||
|
newCards - allCredentials, cardFormats
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if member.schedules != ch.schedules:
|
||||||
|
print(
|
||||||
|
"- Updating schedule for"
|
||||||
|
+ f" {member.forename} {member.surname}:"
|
||||||
|
+ f" {ch.schedules} -> {member.schedules}"
|
||||||
|
)
|
||||||
|
door.doXMLRequest(ROOT(member.make_schedules(schedulesMap)))
|
||||||
|
|
||||||
|
# TODO: delete cardholders that are no longer members?
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config = Config()
|
||||||
|
membershipworks = config.membershipworks
|
||||||
|
membershipworks_attributes = (
|
||||||
|
"_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf,xeh,xse"
|
||||||
|
)
|
||||||
|
|
||||||
|
memberData = membershipworks.get_members(
|
||||||
|
["Members", "CMS Staff", "Misc. Access"], membershipworks_attributes
|
||||||
|
)
|
||||||
|
members = [MembershipworksMember(config, m) for m in memberData]
|
||||||
|
|
||||||
|
formerMemberData = membershipworks.get_members(
|
||||||
|
["Former Members"], membershipworks_attributes
|
||||||
|
)
|
||||||
|
formerMembers = [
|
||||||
|
MembershipworksMember(config, m, formerMember=True) for m in formerMemberData
|
||||||
|
]
|
||||||
|
|
||||||
|
for formerMember in formerMembers:
|
||||||
|
member = next(
|
||||||
|
(
|
||||||
|
m
|
||||||
|
for m in members
|
||||||
|
if m.membershipWorksID == formerMember.membershipWorksID
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# member exists in another folder
|
||||||
|
if member is not None:
|
||||||
|
member.formerMember = True
|
||||||
|
else: # member is only a former member
|
||||||
|
formerMember.formerMember = True
|
||||||
|
members.append(formerMember)
|
||||||
|
|
||||||
|
for door in config.doors.values():
|
||||||
|
print(door.name, door.ip)
|
||||||
|
update_door(door, members)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
41
memberPlumbing/hid/Credential.py
Normal file
41
memberPlumbing/hid/Credential.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import bitstring
|
||||||
|
|
||||||
|
# Reference for H10301 card format:
|
||||||
|
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
|
||||||
|
|
||||||
|
|
||||||
|
class Credential:
|
||||||
|
def __init__(self, code=None, hex=None):
|
||||||
|
if code is None and hex is None:
|
||||||
|
raise TypeError("Must set either code or hex for a Credential")
|
||||||
|
elif code is not None and hex is not None:
|
||||||
|
raise TypeError("Cannot set both code and hex for a Credential")
|
||||||
|
elif code is not None:
|
||||||
|
self.bits = bitstring.pack(
|
||||||
|
"0b000000, 0b0, uint:8=facility, uint:16=number, 0b0",
|
||||||
|
facility=code[0],
|
||||||
|
number=code[1],
|
||||||
|
)
|
||||||
|
self.bits[6] = self.bits[7:19].count(1) % 2 # even parity
|
||||||
|
self.bits[31] = not (self.bits[19:31].count(1) % 2) # odd parity
|
||||||
|
elif hex is not None:
|
||||||
|
self.bits = bitstring.Bits(hex=hex)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Credential({self.code})"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.bits == other.bits
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self.bits.int
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code(self):
|
||||||
|
facility = self.bits[7:15].uint
|
||||||
|
code = self.bits[15:31].uint
|
||||||
|
return (facility, code)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hex(self):
|
||||||
|
return self.bits.hex.upper()
|
223
memberPlumbing/hid/DoorController.py
Normal file
223
memberPlumbing/hid/DoorController.py
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
import csv
|
||||||
|
from datetime import datetime
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import urllib3
|
||||||
|
from lxml import etree
|
||||||
|
from lxml.builder import ElementMaker
|
||||||
|
|
||||||
|
E_plain = ElementMaker(nsmap={"hid": "http://www.hidglobal.com/VertX"})
|
||||||
|
E = ElementMaker(
|
||||||
|
namespace="http://www.hidglobal.com/VertX",
|
||||||
|
nsmap={"hid": "http://www.hidglobal.com/VertX"},
|
||||||
|
)
|
||||||
|
E_corp = ElementMaker(
|
||||||
|
namespace="http://www.hidcorp.com/VertX", # stupid
|
||||||
|
nsmap={"hid": "http://www.hidcorp.com/VertX"},
|
||||||
|
)
|
||||||
|
ROOT = E_plain.VertXMessage
|
||||||
|
|
||||||
|
fieldnames = "CardNumber,CardFormat,PinRequired,PinCode,ExtendedAccess,ExpiryDate,Forename,Initial,Surname,Email,Phone,Custom1,Custom2,Schedule1,Schedule2,Schedule3,Schedule4,Schedule5,Schedule6,Schedule7,Schedule8".split(
|
||||||
|
","
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: where should this live?
|
||||||
|
# it's fine, ssl certs are for losers anyway
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteError(Exception):
|
||||||
|
def __init__(self, r):
|
||||||
|
super().__init__(f"Door Updating Error: {r.status_code} {r.reason}\n{r.text}")
|
||||||
|
|
||||||
|
|
||||||
|
class DoorController:
|
||||||
|
def __init__(self, ip, username, password, name="", access=""):
|
||||||
|
self.ip = ip
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.name = name
|
||||||
|
self.access = access
|
||||||
|
|
||||||
|
def doImport(self, params=None, files=None):
|
||||||
|
"""Send a request to the door control import script"""
|
||||||
|
r = requests.post(
|
||||||
|
"https://" + self.ip + "/cgi-bin/import.cgi",
|
||||||
|
params=params,
|
||||||
|
files=files,
|
||||||
|
auth=requests.auth.HTTPDigestAuth(self.username, self.password),
|
||||||
|
timeout=60,
|
||||||
|
verify=False,
|
||||||
|
) # ignore insecure SSL
|
||||||
|
xml = etree.XML(r.content)
|
||||||
|
if (
|
||||||
|
r.status_code != 200
|
||||||
|
or len(xml.findall("{http://www.hidglobal.com/VertX}Error")) > 0
|
||||||
|
):
|
||||||
|
raise RemoteError(r)
|
||||||
|
|
||||||
|
def doCSVImport(self, csv):
|
||||||
|
"""Do the CSV import procedure on a door control"""
|
||||||
|
self.doImport({"task": "importInit"})
|
||||||
|
self.doImport(
|
||||||
|
{"task": "importCardsPeople", "name": "cardspeopleschedule.csv"},
|
||||||
|
{"importCardsPeopleButton": ("cardspeopleschedule.csv", csv, "text/csv")},
|
||||||
|
)
|
||||||
|
self.doImport({"task": "importDone"})
|
||||||
|
|
||||||
|
def doXMLRequest(self, xml, prefix=b'<?xml version="1.0" encoding="UTF-8"?>'):
|
||||||
|
if not isinstance(xml, bytes):
|
||||||
|
xml = etree.tostring(xml)
|
||||||
|
r = requests.get(
|
||||||
|
"https://" + self.ip + "/cgi-bin/vertx_xml.cgi",
|
||||||
|
params={"XML": prefix + xml},
|
||||||
|
auth=requests.auth.HTTPDigestAuth(self.username, self.password),
|
||||||
|
verify=False,
|
||||||
|
)
|
||||||
|
resp_xml = etree.XML(r.content)
|
||||||
|
# probably meed to be more sane about this
|
||||||
|
if r.status_code != 200 or len(resp_xml.findall("{*}Error")) > 0:
|
||||||
|
raise RemoteError(r)
|
||||||
|
return resp_xml
|
||||||
|
|
||||||
|
def get_scheduleMap(self):
|
||||||
|
schedules = self.doXMLRequest(
|
||||||
|
ROOT(E.Schedules({"action": "LR", "recordOffset": "0", "recordCount": "8"}))
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
fmt.attrib["scheduleName"]: fmt.attrib["scheduleID"] for fmt in schedules[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_schedules(self):
|
||||||
|
# TODO: might be able to do in one request
|
||||||
|
schedules = self.doXMLRequest(ROOT(E.Schedules({"action": "LR"})))
|
||||||
|
etree.dump(schedules)
|
||||||
|
|
||||||
|
data = self.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
*[
|
||||||
|
E.Schedules(
|
||||||
|
{"action": "LR", "scheduleID": schedule.attrib["scheduleID"]}
|
||||||
|
)
|
||||||
|
for schedule in schedules[0]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return ROOT(E_corp.Schedules({"action": "AD"}, *[s[0] for s in data]))
|
||||||
|
|
||||||
|
def set_schedules(self, schedules):
|
||||||
|
# clear all people
|
||||||
|
outString = StringIO()
|
||||||
|
writer = csv.DictWriter(outString, fieldnames)
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerow({})
|
||||||
|
outString.seek(0)
|
||||||
|
self.doCSVImport(outString)
|
||||||
|
|
||||||
|
# clear all schedules
|
||||||
|
delXML = ROOT(
|
||||||
|
*[
|
||||||
|
E.Schedules({"action": "DD", "scheduleID": str(ii)})
|
||||||
|
for ii in range(1, 8)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.doXMLRequest(delXML)
|
||||||
|
except RemoteError:
|
||||||
|
# don't care about failure to delete, they probably just didn't exist
|
||||||
|
pass
|
||||||
|
|
||||||
|
# load new schedules
|
||||||
|
self.doXMLRequest(schedules)
|
||||||
|
|
||||||
|
def get_cardFormats(self):
|
||||||
|
cardFormats = self.doXMLRequest(
|
||||||
|
ROOT(E.CardFormats({"action": "LR", "responseFormat": "expanded"}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
fmt[0].attrib["value"]: fmt.attrib["formatID"]
|
||||||
|
for fmt in cardFormats[0].findall("{*}CardFormat[{*}FixedField]")
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_cardFormat(self, formatName, templateID, facilityCode):
|
||||||
|
# TODO: add ability to delete formats
|
||||||
|
# delete example: <hid:CardFormats action="DD" formatID="7-1-244"/>
|
||||||
|
|
||||||
|
el = ROOT(
|
||||||
|
E.CardFormats(
|
||||||
|
{"action": "AD"},
|
||||||
|
E.CardFormat(
|
||||||
|
{"formatName": formatName, "templateID": str(templateID)},
|
||||||
|
E.FixedField({"value": str(facilityCode)}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return self.doXMLRequest(el)
|
||||||
|
|
||||||
|
def get_records(self, req, count, params={}, stopFunction=None):
|
||||||
|
result = []
|
||||||
|
recordCount = 0
|
||||||
|
moreRecords = True
|
||||||
|
|
||||||
|
# note: all the "+/-1" bits are to work around a bug where the
|
||||||
|
# last returned entry is incomplete. There is probably a
|
||||||
|
# better way to do this, but for now I just get the last entry
|
||||||
|
# again in the next request. I suspect this probably ends
|
||||||
|
# poorly if the numbers line up poorly (ie an exact multiple
|
||||||
|
# of the returned record limit)
|
||||||
|
while moreRecords and (stopFunction is None or stopFunction(result)):
|
||||||
|
res = self.doXMLRequest(
|
||||||
|
ROOT(
|
||||||
|
req(
|
||||||
|
{
|
||||||
|
"action": "LR",
|
||||||
|
"recordCount": str(count - recordCount + 1),
|
||||||
|
"recordOffset": str(
|
||||||
|
recordCount - 1 if recordCount > 0 else 0
|
||||||
|
),
|
||||||
|
**params,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = result[:-1] + list(res[0])
|
||||||
|
|
||||||
|
recordCount += int(res[0].get("recordCount")) - 1
|
||||||
|
moreRecords = res[0].get("moreRecords") == "true"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_cardholders(self):
|
||||||
|
return self.get_records(E.Cardholders, 1000, {"responseFormat": "expanded"})
|
||||||
|
|
||||||
|
def get_credentials(self):
|
||||||
|
return self.get_records(E.Credentials, 1000)
|
||||||
|
|
||||||
|
def get_events(self, threshold):
|
||||||
|
def event_newer_than_threshold(event):
|
||||||
|
return datetime.fromisoformat(event.attrib["timestamp"]) > threshold
|
||||||
|
|
||||||
|
def last_event_newer_than_threshold(events):
|
||||||
|
return (not events) or event_newer_than_threshold(events[-1])
|
||||||
|
|
||||||
|
return [
|
||||||
|
event
|
||||||
|
for event in self.get_records(
|
||||||
|
E.EventMessages, 10000, stopFunction=last_event_newer_than_threshold
|
||||||
|
)
|
||||||
|
if event_newer_than_threshold(event)
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_lock(self):
|
||||||
|
el = ROOT(E.Doors({"action": "LR", "responseFormat": "status"}))
|
||||||
|
xml = self.doXMLRequest(el)
|
||||||
|
relayState = xml.find("./{*}Doors/{*}Door").attrib["relayState"]
|
||||||
|
return "unlocked" if relayState == "set" else "locked"
|
||||||
|
|
||||||
|
def set_lock(self, lock=True):
|
||||||
|
el = ROOT(
|
||||||
|
E.Doors({"action": "CM", "command": "lockDoor" if lock else "unlockDoor"})
|
||||||
|
)
|
||||||
|
return self.doXMLRequest(el)
|
0
memberPlumbing/hid/__init__.py
Normal file
0
memberPlumbing/hid/__init__.py
Normal file
112
memberPlumbing/hidEvents.py
Executable file
112
memberPlumbing/hidEvents.py
Executable file
@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from lxml import etree
|
||||||
|
from peewee import (
|
||||||
|
BooleanField,
|
||||||
|
CharField,
|
||||||
|
CompositeKey,
|
||||||
|
DateTimeField,
|
||||||
|
IntegerField,
|
||||||
|
Model,
|
||||||
|
MySQLDatabase,
|
||||||
|
TextField,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
database = MySQLDatabase(None)
|
||||||
|
|
||||||
|
|
||||||
|
class HIDEvent(Model):
|
||||||
|
doorName = CharField()
|
||||||
|
timestamp = DateTimeField()
|
||||||
|
eventType = IntegerField()
|
||||||
|
readerAddress = IntegerField()
|
||||||
|
cardholderID = IntegerField(null=True)
|
||||||
|
commandStatus = BooleanField(null=True)
|
||||||
|
forename = TextField(null=True)
|
||||||
|
surname = TextField(null=True)
|
||||||
|
ioState = BooleanField(null=True)
|
||||||
|
newTime = DateTimeField(null=True)
|
||||||
|
oldTime = DateTimeField(null=True)
|
||||||
|
rawCardNumber = CharField(max_length=8, null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
primary_key = CompositeKey("doorName", "timestamp", "eventType")
|
||||||
|
database = database
|
||||||
|
|
||||||
|
|
||||||
|
def getStrings(door):
|
||||||
|
"""Parses out the message strings from source."""
|
||||||
|
r = requests.get(
|
||||||
|
"https://" + door.ip + "/html/en_EN/en_EN.js",
|
||||||
|
auth=requests.auth.HTTPDigestAuth(door.username, door.password),
|
||||||
|
verify=False,
|
||||||
|
)
|
||||||
|
regex = re.compile(r'([0-9]+)="([^"]*)')
|
||||||
|
strings = [
|
||||||
|
regex.search(s)
|
||||||
|
for s in r.text.split(";")
|
||||||
|
if s.startswith("localeStrings.eventDetails")
|
||||||
|
]
|
||||||
|
print({int(g.group(1)): g.group(2) for g in strings})
|
||||||
|
|
||||||
|
|
||||||
|
@database.atomic()
|
||||||
|
def getMessages(door):
|
||||||
|
last_event = (
|
||||||
|
HIDEvent.select(HIDEvent.timestamp)
|
||||||
|
.where(HIDEvent.doorName == door.name)
|
||||||
|
.order_by(HIDEvent.timestamp.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if last_event is not None:
|
||||||
|
last_ts = last_event.timestamp
|
||||||
|
else:
|
||||||
|
last_ts = datetime(2010, 1, 1)
|
||||||
|
|
||||||
|
events = door.get_events(last_ts)
|
||||||
|
|
||||||
|
HIDEvent.insert_many(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
# fill with None, then overwrite with real contents
|
||||||
|
**{k: None for k in HIDEvent._meta.fields.keys()},
|
||||||
|
**event.attrib,
|
||||||
|
"doorName": door.name,
|
||||||
|
}
|
||||||
|
for event in events
|
||||||
|
]
|
||||||
|
).on_conflict_ignore().execute()
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
|
def dups(events):
|
||||||
|
timestamps = [e.attrib["timestamp"] for e in events]
|
||||||
|
dups = set([x for x in timestamps if timestamps.count(x) > 1])
|
||||||
|
for e in events:
|
||||||
|
if e.attrib["timestamp"] in dups:
|
||||||
|
etree.dump(e)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config = Config()
|
||||||
|
database.init(
|
||||||
|
**config.HID_DB,
|
||||||
|
**{
|
||||||
|
"charset": "utf8",
|
||||||
|
"sql_mode": "PIPES_AS_CONCAT",
|
||||||
|
"use_unicode": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HIDEvent.create_table()
|
||||||
|
for door in config.doors.values():
|
||||||
|
getMessages(door)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
248
memberPlumbing/mw_models.py
Normal file
248
memberPlumbing/mw_models.py
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from peewee import (
|
||||||
|
BooleanField,
|
||||||
|
CompositeKey,
|
||||||
|
DateField,
|
||||||
|
DateTimeField,
|
||||||
|
DecimalField,
|
||||||
|
CharField,
|
||||||
|
FixedCharField,
|
||||||
|
ForeignKeyField,
|
||||||
|
Model,
|
||||||
|
MySQLDatabase,
|
||||||
|
TextField,
|
||||||
|
)
|
||||||
|
|
||||||
|
database = MySQLDatabase(None)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseModel(Model):
|
||||||
|
_csv_headers_override = {}
|
||||||
|
_date_fields = {}
|
||||||
|
|
||||||
|
def insert_instance(self):
|
||||||
|
return self.insert(**self.__data__)
|
||||||
|
|
||||||
|
def upsert_instance(self):
|
||||||
|
return self.insert_instance().on_conflict(
|
||||||
|
action="update", preserve=list(self._meta.fields.values())
|
||||||
|
)
|
||||||
|
|
||||||
|
def magic_save(self):
|
||||||
|
if self._meta.primary_key is False:
|
||||||
|
self.get_or_create(**self.__data__)
|
||||||
|
else:
|
||||||
|
self.upsert_instance().execute()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _headers_map(cls):
|
||||||
|
return {field.column_name: name for name, field in cls._meta.fields.items()}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _remap_headers(cls, data):
|
||||||
|
# print(data)
|
||||||
|
hmap = cls._headers_map()
|
||||||
|
hmap.update(cls._csv_headers_override)
|
||||||
|
for k, v in data.items():
|
||||||
|
if k in hmap:
|
||||||
|
yield hmap.get(k), v
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_csv_dict(cls, data):
|
||||||
|
data = data.copy()
|
||||||
|
|
||||||
|
# parse date fields to datetime objects
|
||||||
|
for field, fmt in cls._date_fields.items():
|
||||||
|
if data[field]:
|
||||||
|
data[field] = datetime.strptime(str(data[field]), fmt)
|
||||||
|
else:
|
||||||
|
# convert empty string to None to make NULL in SQL
|
||||||
|
data[field] = None
|
||||||
|
|
||||||
|
return cls(**dict(cls._remap_headers(data)))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = database
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: is this still a temporal table?
|
||||||
|
class Member(BaseModel):
|
||||||
|
uid = FixedCharField(24, primary_key=True)
|
||||||
|
year_of_birth = TextField(column_name="Year of Birth", null=True)
|
||||||
|
account_name = TextField(column_name="Account Name", null=True)
|
||||||
|
first_name = TextField(column_name="First Name", null=True)
|
||||||
|
last_name = TextField(column_name="Last Name", null=True)
|
||||||
|
phone = TextField(column_name="Phone", null=True)
|
||||||
|
email = TextField(column_name="Email", null=True)
|
||||||
|
address_street = TextField(column_name="Address (Street)", null=True)
|
||||||
|
address_city = TextField(column_name="Address (City)", null=True)
|
||||||
|
address_state_province = TextField(
|
||||||
|
column_name="Address (State/Province)", null=True
|
||||||
|
)
|
||||||
|
address_postal_code = TextField(column_name="Address (Postal Code)", null=True)
|
||||||
|
address_country = TextField(column_name="Address (Country)", null=True)
|
||||||
|
profile_description = TextField(column_name="Profile description", null=True)
|
||||||
|
website = TextField(column_name="Website", null=True)
|
||||||
|
fax = TextField(column_name="Fax", null=True)
|
||||||
|
contact_person = TextField(column_name="Contact Person", null=True)
|
||||||
|
password = TextField(column_name="Password", null=True)
|
||||||
|
position_relation = TextField(column_name="Position/relation", null=True)
|
||||||
|
parent_account_id = TextField(column_name="Parent Account ID", null=True)
|
||||||
|
gift_membership_purchased_by = TextField(
|
||||||
|
column_name="Gift Membership purchased by", null=True
|
||||||
|
)
|
||||||
|
purchased_gift_membership_for = TextField(
|
||||||
|
column_name="Purchased Gift Membership for", null=True
|
||||||
|
)
|
||||||
|
closet_storage = TextField(column_name="Closet Storage #", null=True)
|
||||||
|
storage_shelf = TextField(column_name="Storage Shelf #", null=True)
|
||||||
|
personal_studio_space = TextField(column_name="Personal Studio Space #", null=True)
|
||||||
|
access_permitted_shops_during_extended_hours = BooleanField(
|
||||||
|
column_name="Access Permitted Shops During Extended Hours?"
|
||||||
|
)
|
||||||
|
normal_access_permitted_during_covid19_limited_operations = BooleanField(
|
||||||
|
column_name="Normal Access Permitted During COVID-19 Limited Operations"
|
||||||
|
)
|
||||||
|
access_permitted_during_covid19_staffed_period_only = BooleanField(
|
||||||
|
column_name="Access Permitted During COVID-19 Staffed Period Only"
|
||||||
|
)
|
||||||
|
access_front_door_and_studio_space_during_extended_hours = BooleanField(
|
||||||
|
column_name="Access Front Door and Studio Space During Extended Hours?"
|
||||||
|
)
|
||||||
|
access_wood_shop = BooleanField(column_name="Access Wood Shop?")
|
||||||
|
access_metal_shop = BooleanField(column_name="Access Metal Shop?")
|
||||||
|
access_storage_closet = BooleanField(column_name="Access Storage Closet?")
|
||||||
|
access_studio_space = BooleanField(column_name="Access Studio Space?")
|
||||||
|
access_front_door = BooleanField(column_name="Access Front Door?")
|
||||||
|
access_card_number = TextField(column_name="Access Card Number", null=True)
|
||||||
|
access_card_facility_code = TextField(
|
||||||
|
column_name="Access Card Facility Code", null=True
|
||||||
|
)
|
||||||
|
auto_billing_id = TextField(column_name="Auto Billing ID", null=True)
|
||||||
|
billing_method = TextField(column_name="Billing Method", null=True)
|
||||||
|
renewal_date = DateField(column_name="Renewal Date", null=True)
|
||||||
|
join_date = DateField(column_name="Join Date", null=True)
|
||||||
|
admin_note = TextField(column_name="Admin note", null=True)
|
||||||
|
profile_gallery_image_url = TextField(
|
||||||
|
column_name="Profile gallery image URL", null=True
|
||||||
|
)
|
||||||
|
business_card_image_url = TextField(
|
||||||
|
column_name="Business card image URL", null=True
|
||||||
|
)
|
||||||
|
instagram = TextField(column_name="Instagram", null=True)
|
||||||
|
pinterest = TextField(column_name="Pinterest", null=True)
|
||||||
|
youtube = TextField(column_name="Youtube", null=True)
|
||||||
|
yelp = TextField(column_name="Yelp", null=True)
|
||||||
|
google = TextField(column_name="Google+", null=True)
|
||||||
|
bbb = TextField(column_name="BBB", null=True)
|
||||||
|
twitter = TextField(column_name="Twitter", null=True)
|
||||||
|
facebook = TextField(column_name="Facebook", null=True)
|
||||||
|
linked_in = TextField(column_name="LinkedIn", null=True)
|
||||||
|
do_not_show_street_address_in_profile = TextField(
|
||||||
|
column_name="Do not show street address in profile", null=True
|
||||||
|
)
|
||||||
|
do_not_list_in_directory = TextField(
|
||||||
|
column_name="Do not list in directory", null=True
|
||||||
|
)
|
||||||
|
how_did_you_hear = TextField(column_name="HowDidYouHear", null=True)
|
||||||
|
authorize_charge = TextField(column_name="authorizeCharge", null=True)
|
||||||
|
policy_agreement = TextField(column_name="policyAgreement", null=True)
|
||||||
|
waiver_form_signed_and_on_file_date = DateField(
|
||||||
|
column_name="Waiver form signed and on file date.", null=True
|
||||||
|
)
|
||||||
|
membership_agreement_signed_and_on_file_date = DateField(
|
||||||
|
column_name="Membership Agreement signed and on file date.", null=True
|
||||||
|
)
|
||||||
|
ip_address = TextField(column_name="IP Address", null=True)
|
||||||
|
audit_date = DateField(column_name="Audit Date", null=True)
|
||||||
|
agreement_version = TextField(column_name="Agreement Version", null=True)
|
||||||
|
paperwork_status = TextField(column_name="Paperwork status", null=True)
|
||||||
|
membership_agreement_dated = BooleanField(column_name="Membership agreement dated")
|
||||||
|
membership_agreement_acknowledgement_page_filled_out = BooleanField(
|
||||||
|
column_name="Membership Agreement Acknowledgement Page Filled Out"
|
||||||
|
)
|
||||||
|
membership_agreement_signed = BooleanField(
|
||||||
|
column_name="Membership Agreement Signed"
|
||||||
|
)
|
||||||
|
liability_form_filled_out = BooleanField(column_name="Liability Form Filled Out")
|
||||||
|
self_certify_essential_business = BooleanField(
|
||||||
|
column_name="selfCertifyEssentialBusiness"
|
||||||
|
)
|
||||||
|
accepted_covid19_policy = BooleanField(column_name="Accepted COVID-19 Policy")
|
||||||
|
|
||||||
|
_csv_headers_override = {
|
||||||
|
"Account ID": "uid",
|
||||||
|
"Please tell us how you heard about the Claremont MakerSpace and what tools or shops you are most excited to start using:": "how_did_you_hear",
|
||||||
|
"Yes - I authorize TwinState MakerSpaces, Inc. to charge my credit card for the membership and other options that I have selected.": "authorize_charge",
|
||||||
|
"I have read the Claremont MakerSpace Membership Agreement & Policies, and agree to all terms stated therein.": "policy_agreement",
|
||||||
|
"Access Permitted Using Membership Level Schedule During COVID-19 Limited Operations": "normal_access_permitted_during_covid19_limited_operations",
|
||||||
|
"I hereby certify that I am involved in Essential Business as defined by the State of New Hampshire and that I will follow the practices identified in the State of New Hampshire Exhibit C to Emergency Order #40 (Section E. Manufacturing).": "self_certify_essential_business",
|
||||||
|
"I have read, and agree to abide by the terms, conditions, policies and procedures contained in the Claremont MakerSpace COVID-19 Policies and Procedures.": "accepted_covid19_policy",
|
||||||
|
}
|
||||||
|
|
||||||
|
_date_fields = {
|
||||||
|
"Join Date": "%b %d, %Y",
|
||||||
|
"Renewal Date": "%b %d, %Y",
|
||||||
|
"Audit Date": "%m/%d/%Y",
|
||||||
|
"Membership Agreement signed and on file date.": "%m/%d/%Y",
|
||||||
|
"Waiver form signed and on file date.": "%m/%d/%Y",
|
||||||
|
}
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "members"
|
||||||
|
|
||||||
|
|
||||||
|
class Flag(BaseModel):
|
||||||
|
id = FixedCharField(24, primary_key=True)
|
||||||
|
name = TextField(null=True)
|
||||||
|
type = CharField(6)
|
||||||
|
|
||||||
|
|
||||||
|
class MemberFlag(BaseModel):
|
||||||
|
uid = ForeignKeyField(Member, column_name="uid", backref="flags")
|
||||||
|
flag_id = ForeignKeyField(Flag, backref="members")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
primary_key = CompositeKey("flag_id", "uid")
|
||||||
|
|
||||||
|
|
||||||
|
class Transaction(BaseModel):
|
||||||
|
sid = FixedCharField(27, null=True)
|
||||||
|
uid = ForeignKeyField(Member, column_name="uid", backref="transactions", null=True)
|
||||||
|
timestamp = DateTimeField()
|
||||||
|
type = TextField(null=True)
|
||||||
|
sum = DecimalField(13, 4, null=True)
|
||||||
|
fee = DecimalField(13, 4, null=True)
|
||||||
|
event_id = TextField(null=True)
|
||||||
|
for_ = TextField(column_name="For", null=True)
|
||||||
|
items = TextField(column_name="Items", null=True)
|
||||||
|
discount_code = TextField(column_name="Discount Code", null=True)
|
||||||
|
note = TextField(column_name="Note", null=True)
|
||||||
|
name = TextField(column_name="Name", null=True)
|
||||||
|
contact_person = TextField(column_name="Contact Person", null=True)
|
||||||
|
full_address = TextField(column_name="Full Address", null=True)
|
||||||
|
street = TextField(column_name="Street", null=True)
|
||||||
|
city = TextField(column_name="City", null=True)
|
||||||
|
state_province = TextField(column_name="State/Province", null=True)
|
||||||
|
postal_code = TextField(column_name="Postal Code", null=True)
|
||||||
|
country = TextField(column_name="Country", null=True)
|
||||||
|
phone = TextField(column_name="Phone", null=True)
|
||||||
|
email = TextField(column_name="Email", null=True)
|
||||||
|
|
||||||
|
_csv_headers_override = {
|
||||||
|
"_dp": "timestamp",
|
||||||
|
"Transaction Type": "type",
|
||||||
|
"Event/Form Name": "for_",
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_csv_dict(cls, data):
|
||||||
|
txn = data.copy()
|
||||||
|
# can't use '%s' format string, have to use the special function
|
||||||
|
txn["_dp"] = datetime.fromtimestamp(txn["_dp"])
|
||||||
|
return super().from_csv_dict(txn)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table_name = "transactions"
|
||||||
|
primary_key = False
|
96
memberPlumbing/sqlExport.py
Executable file
96
memberPlumbing/sqlExport.py
Executable file
@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .mw_models import Member, Flag, MemberFlag, Transaction, database
|
||||||
|
|
||||||
|
|
||||||
|
@database.atomic()
|
||||||
|
def do_import(config):
|
||||||
|
membershipworks = config.membershipworks
|
||||||
|
print("Creating tables")
|
||||||
|
database.create_tables([Member, Flag, MemberFlag, Transaction])
|
||||||
|
|
||||||
|
print("Updating flags (labels, levels, and addons)")
|
||||||
|
flags = membershipworks._parse_flags()
|
||||||
|
Flag.insert_many(
|
||||||
|
[
|
||||||
|
{"id": v, "name": k, "type": typ[:-1]}
|
||||||
|
for typ, flags_of_type in flags.items()
|
||||||
|
for k, v in flags_of_type.items()
|
||||||
|
]
|
||||||
|
).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...")
|
||||||
|
members = membershipworks.get_all_members()
|
||||||
|
for m in members:
|
||||||
|
# replace flags by booleans
|
||||||
|
for flag in [dek["lbl"] for dek in membershipworks.org_info["dek"]]:
|
||||||
|
if flag in m:
|
||||||
|
m[flag] = m[flag] == flag
|
||||||
|
|
||||||
|
for field_id, field in membershipworks._all_fields().items():
|
||||||
|
# convert checkboxes to real booleans
|
||||||
|
if field.get("typ") == 8 and field["lbl"] in m: # check box
|
||||||
|
m[field["lbl"]] = True if m[field["lbl"]] == "Y" else False
|
||||||
|
|
||||||
|
for member in members:
|
||||||
|
# create/update member
|
||||||
|
Member.from_csv_dict(member).magic_save()
|
||||||
|
|
||||||
|
# update member's flags
|
||||||
|
for type, flags in membershipworks._parse_flags().items():
|
||||||
|
for flag, id in flags.items():
|
||||||
|
ml = MemberFlag(uid=member["Account ID"], flag_id=id)
|
||||||
|
if (type == "folders" and member["Account ID"] in folders[id]) or (
|
||||||
|
type != "folders" and member[flag]
|
||||||
|
):
|
||||||
|
ml.magic_save()
|
||||||
|
else:
|
||||||
|
ml.delete_instance()
|
||||||
|
|
||||||
|
print("Getting/Updating transactions...")
|
||||||
|
# Deduping these is hard, so just recreate the data every time
|
||||||
|
Transaction.truncate_table()
|
||||||
|
now = datetime.now()
|
||||||
|
start_date = datetime(2010, 1, 1)
|
||||||
|
transactions_csv = membershipworks.get_transactions(start_date, now)
|
||||||
|
transactions_json = membershipworks.get_transactions(start_date, now, json=True)
|
||||||
|
# this is terrible, but as long as the dates are the same, should be fiiiine
|
||||||
|
transactions = [{**j, **v} for j, v in zip(transactions_csv, transactions_json)]
|
||||||
|
assert all(
|
||||||
|
[
|
||||||
|
t["Account ID"] == t.get("uid", "") and t["Payment ID"] == t.get("sid", "")
|
||||||
|
for t in transactions
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for transaction in transactions:
|
||||||
|
Transaction.from_csv_dict(transaction).magic_save()
|
||||||
|
|
||||||
|
# TODO: folders, levels, addons
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config = Config()
|
||||||
|
database.init(
|
||||||
|
**config.MEMBERSHIPWORKS_DB,
|
||||||
|
**{
|
||||||
|
"charset": "utf8",
|
||||||
|
"sql_mode": "PIPES_AS_CONCAT",
|
||||||
|
"use_unicode": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
do_import(config)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
131
memberPlumbing/ucsAccounts.py
Executable file
131
memberPlumbing/ucsAccounts.py
Executable file
@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
|
||||||
|
from udm_rest_client.udm import UDM
|
||||||
|
from udm_rest_client.exceptions import NoObject, UdmError
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
USER_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||||
|
GROUP_BASE = "cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||||
|
GROUPS_REGEX = "|".join(
|
||||||
|
["Certified: .*", "Access .*\?", "CMS .*", "Volunteer: .*", "Database .*"]
|
||||||
|
)
|
||||||
|
RAND_PW_LEN = 20
|
||||||
|
|
||||||
|
|
||||||
|
# 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 = [
|
||||||
|
sanitize_group_name(group_name)
|
||||||
|
for group_name in members[0].keys()
|
||||||
|
if re.match(GROUPS_REGEX, group_name) is not None
|
||||||
|
]
|
||||||
|
for group_name in groups:
|
||||||
|
if group_name not in existing_group_names:
|
||||||
|
group = await group_mod.new()
|
||||||
|
group.props.name = group_name
|
||||||
|
await group.save()
|
||||||
|
|
||||||
|
|
||||||
|
async def _main():
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
members = config.membershipworks.get_members(
|
||||||
|
["Members", "CMS Staff"], "lvl,phn,eml,lbl,nam,end,_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
async with UDM(**config.UCS) as udm:
|
||||||
|
user_mod = udm.get("users/user")
|
||||||
|
group_mod = udm.get("groups/group")
|
||||||
|
|
||||||
|
await make_groups(group_mod, members)
|
||||||
|
|
||||||
|
for member in members:
|
||||||
|
username = sanitize_user_name(member["Account Name"])
|
||||||
|
|
||||||
|
try: # try to get an existing user to update
|
||||||
|
user = await user_mod.get(f"uid={username},{USER_BASE}")
|
||||||
|
except NoObject: # create a new user
|
||||||
|
# TODO: search by employeeNumber and rename users when needed
|
||||||
|
user = await user_mod.new()
|
||||||
|
|
||||||
|
# set a random password and ensure it is changed at next login
|
||||||
|
user.props.password = "".join(
|
||||||
|
random.choice(string.ascii_letters + string.digits)
|
||||||
|
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__":
|
||||||
|
main()
|
76
memberPlumbing/upcomingEvents.py
Normal file
76
memberPlumbing/upcomingEvents.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .MembershipWorks import MembershipWorks
|
||||||
|
|
||||||
|
|
||||||
|
def format_event(membershipworks: MembershipWorks, event):
|
||||||
|
event_details = membershipworks.get_event_by_eid(event["eid"])
|
||||||
|
url = (
|
||||||
|
"https://claremontmakerspace.org/events/#!event/register/"
|
||||||
|
+ event_details["url"]
|
||||||
|
)
|
||||||
|
if "lgo" in event_details:
|
||||||
|
img = f"""<img class="aligncenter" width="500" height="500" src="{event_details['lgo']['l']}">"""
|
||||||
|
else:
|
||||||
|
img = ""
|
||||||
|
# print(json.dumps(event_details))
|
||||||
|
return f"""<h2 style="text-align: center;">
|
||||||
|
<a href="{url}">
|
||||||
|
{img}
|
||||||
|
{event_details['ttl']}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<div>{event_details['szp']} — {event_details['ezp']}</div>
|
||||||
|
<div>
|
||||||
|
{event_details['dtl']}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{url}">Register for this class now!</a>"""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
membershipworks = config.membershipworks
|
||||||
|
events = membershipworks.get_events_list(datetime.now())
|
||||||
|
if "error" in events:
|
||||||
|
print("Error:", events["error"])
|
||||||
|
return
|
||||||
|
|
||||||
|
events_list = "\n<hr />\n\n".join(
|
||||||
|
format_event(membershipworks, event)
|
||||||
|
for event in events["evt"]
|
||||||
|
if event["ttl"] != "[TEMPLATE FOR COPYING]"
|
||||||
|
)
|
||||||
|
header = """<p><img class="aligncenter size-medium wp-image-2319" src="https://claremontmakerspace.org/wp-content/uploads/2019/03/CMS-Logo-b-y-g-300x168.png" alt="" width="300" height="168" /></a></p>
|
||||||
|
<p>Greetings Upper Valley Makers:</p>
|
||||||
|
<p>We have an exciting list of upcoming classes at the Claremont MakerSpace that we think might interest you.</p>
|
||||||
|
<strong>For most classes and events, CMS MEMBERSHIP IS NOT REQUIRED.</strong> That said, members receive a discount on registration and there are some classes/events that are for members only (this will be clearly noted in the event description).
|
||||||
|
|
||||||
|
<strong>Class policies</strong> (liability waiver, withdrawal, cancellation, etc.) can be found <a href="https://claremontmakerspace.org/class-policies/" data-wpel-link="internal">here</a>.
|
||||||
|
|
||||||
|
<strong>Please note: </strong>The Claremont MakerSpace currently requires masks for all visitors and members.
|
||||||
|
|
||||||
|
<strong>Instructors:</strong> Interested in teaching a class at CMS? Please fill out our <a href="https://docs.google.com/forms/d/e/1FAIpQLSdJyEVRJxzIczG784VkOm_DsNyv-VXRXYzlis8qlMdEOvHGpQ/viewform" target="_blank" rel="noreferrer noopener external" data-wpel-link="external">Class Proposal Form</a><a href="https://docs.google.com/forms/d/e/1FAIpQLSdJyEVRJxzIczG784VkOm_DsNyv-VXRXYzlis8qlMdEOvHGpQ/viewform" target="_blank" rel="noreferrer noopener external" data-wpel-link="external">.</a>
|
||||||
|
|
||||||
|
<strong>Tours:</strong> Want to see what the Claremont MakerSpace is all about? Tours are by appointment only due to COVID-19 restrictions. <a href="https://tickets.claremontmakerspace.org/open.php" target="_blank" rel="noreferrer noopener external" data-wpel-link="external">Contact Us</a> to schedule your tour where you can learn about all the awesome tools that the CMS offers access to, as well as how membership, classes, and studio spaces work.
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
"""
|
||||||
|
|
||||||
|
footer = """
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>Happy Makin’!</div>
|
||||||
|
<div>We are grateful for all of the public support that our 501(c)(3), non-profit organization receives. If you’d like to make a donation,please visit the <a href="https://claremontmakerspace.org/support/"><strong>Support Us page</strong></a> of our website.</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(header, events_list, footer)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -1,71 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import re
|
|
||||||
import http
|
|
||||||
|
|
||||||
from flask import Flask, render_template, request
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
from common import *
|
|
||||||
from hid.DoorController import DoorController
|
|
||||||
|
|
||||||
def parse_list(member, regex):
|
|
||||||
data_list = []
|
|
||||||
for key, value in member.items():
|
|
||||||
match = re.match(regex, key)
|
|
||||||
if match is not None and value != '':
|
|
||||||
data_list.append(match.group(1))
|
|
||||||
|
|
||||||
return ", ".join(data_list)
|
|
||||||
|
|
||||||
def parse_members(members):
|
|
||||||
data = []
|
|
||||||
for member in members:
|
|
||||||
props = {
|
|
||||||
'Name': member['Account Name'],
|
|
||||||
'Renewal Date': member['Renewal Date'],
|
|
||||||
'Card Number': member['Access Card Facility Code'] + '-' \
|
|
||||||
+ member['Access Card Number'],
|
|
||||||
'Account on Hold': "Yes" if member['Account on Hold'] != '' else "No" }
|
|
||||||
|
|
||||||
props['Certifications'] = parse_list(member, 'Certified: (.*)')
|
|
||||||
props['Door Access'] = parse_list(member, 'Access (.*)\?')
|
|
||||||
props['Memebership Level'] = parse_list(member, 'CMS (.*)')
|
|
||||||
|
|
||||||
data.append(props)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def main():
|
|
||||||
# maybe not now: membership agreement signed
|
|
||||||
# TODO: renewal date check
|
|
||||||
term = request.args.get('term', '')
|
|
||||||
|
|
||||||
if len(term) < 3:
|
|
||||||
return render_template("members.html",
|
|
||||||
error="Enter at least 3 characters to search")
|
|
||||||
|
|
||||||
data = getMembershipworksData(
|
|
||||||
['members', 'staff'],
|
|
||||||
"lvl,xws,xms,xsc,xas,xfd,xac,phn,eml,lbl,xcf,nam,end")
|
|
||||||
|
|
||||||
members = parse_members(data)
|
|
||||||
members = [member for member in members
|
|
||||||
if term.lower() in member['Name'].lower()]
|
|
||||||
headers = ['Name', 'Certifications', 'Door Access', 'Memebership Level',
|
|
||||||
'Card Number', 'Renewal Date', 'Account on Hold']
|
|
||||||
|
|
||||||
if len(members) > 4:
|
|
||||||
return render_template(
|
|
||||||
"members.html", error="Too many results, please be more specific.")
|
|
||||||
|
|
||||||
return render_template("members.html", headers=headers, members=members)
|
|
||||||
|
|
||||||
@app.route('/frontDoor/<lock>', methods=['POST'])
|
|
||||||
def unlockLockDoor(lock):
|
|
||||||
doors['Front Door'].lockOrUnlockDoor(lock != 'unlock')
|
|
||||||
return ('', http.HTTPStatus.NO_CONTENT)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(debug=True, host='0.0.0.0')
|
|
110
old_xml_based.py
110
old_xml_based.py
@ -1,110 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import requests
|
|
||||||
from xml.etree import ElementTree as ET
|
|
||||||
import csv
|
|
||||||
|
|
||||||
from passwords import *
|
|
||||||
|
|
||||||
#credentialRanges = {"6": {"min": "20176", "max": "20350"}}
|
|
||||||
|
|
||||||
memberLevels = ["CMS Staff",
|
|
||||||
"CMS Weekends Only",
|
|
||||||
"CMS Weekdays Only",
|
|
||||||
"CMS Unlimited",
|
|
||||||
"CMS Nights & Weekends"]
|
|
||||||
|
|
||||||
XML = ET.Element("VertXMessage")
|
|
||||||
#TODO: both those might need more stuff:
|
|
||||||
# recordOffset="0" recordCount="4" moreRecords="false"
|
|
||||||
cardholders = ET.SubElement(XML, "hid:Cardholders",
|
|
||||||
attrib={"action": "AD", "recordOffset": "0"})
|
|
||||||
credentials = ET.SubElement(XML, "hid:Credentials", attrib={"action": "AD"})
|
|
||||||
|
|
||||||
def doRequest(xml):
|
|
||||||
return requests.get(
|
|
||||||
'https://172.18.51.11/cgi-bin/vertx_xml.cgi',
|
|
||||||
params={'XML': b'<?xml version="1.0" encoding="UTF-8"?>' + ET.tostring(xml)},
|
|
||||||
auth=requests.auth.HTTPDigestAuth(DOOR_USERNAME, DOOR_PASSWORD),
|
|
||||||
verify=False)
|
|
||||||
|
|
||||||
|
|
||||||
def makeRoleSet(roleSetID, scheduleID):
|
|
||||||
roleSet = ET.SubElement(XML, "hid:RoleSet", attrib={"action": "UD",
|
|
||||||
"roleSetID": str(roleSetID)})
|
|
||||||
roles = ET.SubElement(roleSet, "hid:Roles")
|
|
||||||
ET.SubElement(roles, "hid:Role", attrib={"roleID": str(roleSetID),
|
|
||||||
"scheduleID": str(scheduleID),
|
|
||||||
"resourceID": "0"})
|
|
||||||
|
|
||||||
def makeCardHolder(id, fname, lname):
|
|
||||||
attrib={"cardhlderID": str(id),
|
|
||||||
"forename": fname,
|
|
||||||
"surname": lname,
|
|
||||||
# "email": "", #TODO
|
|
||||||
# "phone": "", #TODO
|
|
||||||
"roleSetID": str(id)}
|
|
||||||
ET.SubElement(cardholders, "hid:Cardholder", attrib=attrib)
|
|
||||||
|
|
||||||
def makeCredential(cardNum, cardHolderID):
|
|
||||||
ET.SubElement(credentials, "hid:Credential",
|
|
||||||
attrib={"isCard": "true", #TODO: needed?
|
|
||||||
"cardNumber": str(cardNum),
|
|
||||||
"cardholderID": str(cardHolderID),
|
|
||||||
"formatID": "6"})
|
|
||||||
|
|
||||||
def handleRow(index, row):
|
|
||||||
makeCardHolder(index, row["First Name"], row["Last Name"])
|
|
||||||
|
|
||||||
memberLevel = [roleSetID for roleSetID, name in enumerate(memberLevels, 1)
|
|
||||||
if row[name] != ""]
|
|
||||||
if len(memberLevel) == 1:
|
|
||||||
makeRoleSet(index, memberLevel[0])
|
|
||||||
else:
|
|
||||||
print(row["First Name"], row["Last Name"], "has no/too many member levels!")
|
|
||||||
|
|
||||||
if row["Access Card Number"] != "":
|
|
||||||
makeCredential(row["Access Card Number"], index)
|
|
||||||
|
|
||||||
def deleteStuff():
|
|
||||||
delXML = ET.Element("VertXMessage")
|
|
||||||
queryXML = ET.Element("VertXMessage")
|
|
||||||
ET.SubElement(queryXML, "hid:Credentials", attrib={"action": "LR"})
|
|
||||||
r = doRequest(queryXML)
|
|
||||||
respXML = ET.XML(r.text)
|
|
||||||
|
|
||||||
for cred in respXML[0]:
|
|
||||||
ET.SubElement(delXML, "hid:Credentials",
|
|
||||||
attrib={"action": "DD",
|
|
||||||
"isCard": "true",
|
|
||||||
"rawCardNumber": cred.attrib["rawCardNumber"]})
|
|
||||||
|
|
||||||
for ii in range(1, 300):
|
|
||||||
ET.SubElement(delXML, "hid:Cardholders",
|
|
||||||
attrib={"action": "DD", "cardholderID": str(ii)})
|
|
||||||
|
|
||||||
r = doRequest(delXML)
|
|
||||||
print(r.text)
|
|
||||||
|
|
||||||
def main():
|
|
||||||
deleteStuff()
|
|
||||||
|
|
||||||
# Include schedules.xml fragment
|
|
||||||
# with open("schedules.xml") as f:
|
|
||||||
# root = ET.XML(f.read())
|
|
||||||
# for e in root:
|
|
||||||
# XML.append(e)
|
|
||||||
|
|
||||||
with open("export-16.csv") as f:
|
|
||||||
reader = csv.DictReader(f)
|
|
||||||
for index, row in enumerate(reader, 1):
|
|
||||||
handleRow(index, row)
|
|
||||||
|
|
||||||
with open("/home/adam/scratch/test.xml", "wb") as f:
|
|
||||||
f.write(ET.tostring(XML))
|
|
||||||
|
|
||||||
r = doRequest(XML)
|
|
||||||
print(r.status_code, r.text)
|
|
||||||
|
|
||||||
# if __name__ == '__main__':
|
|
||||||
# main()
|
|
||||||
main()
|
|
955
poetry.lock
generated
Normal file
955
poetry.lock
generated
Normal file
@ -0,0 +1,955 @@
|
|||||||
|
[[package]]
|
||||||
|
name = "aiohttp"
|
||||||
|
version = "3.8.1"
|
||||||
|
description = "Async http client/server framework (asyncio)"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
aiosignal = ">=1.1.2"
|
||||||
|
async-timeout = ">=4.0.0a3,<5.0"
|
||||||
|
asynctest = {version = "0.13.0", markers = "python_version < \"3.8\""}
|
||||||
|
attrs = ">=17.3.0"
|
||||||
|
charset-normalizer = ">=2.0,<3.0"
|
||||||
|
frozenlist = ">=1.1.1"
|
||||||
|
multidict = ">=4.5,<7.0"
|
||||||
|
typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
|
||||||
|
yarl = ">=1.0,<2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
speedups = ["aiodns", "brotli", "cchardet"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiosignal"
|
||||||
|
version = "1.2.0"
|
||||||
|
description = "aiosignal: a list of registered asynchronous callbacks"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
frozenlist = ">=1.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-property"
|
||||||
|
version = "0.2.1"
|
||||||
|
description = "Python decorator for async properties."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-timeout"
|
||||||
|
version = "4.0.2"
|
||||||
|
description = "Timeout context manager for asyncio programs"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
typing-extensions = {version = ">=3.6.5", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asynctest"
|
||||||
|
version = "0.13.0"
|
||||||
|
description = "Enhance the standard unittest package with features for testing asyncio libraries"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "21.4.0"
|
||||||
|
description = "Classes Without Boilerplate"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
|
||||||
|
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||||
|
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
|
||||||
|
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitstring"
|
||||||
|
version = "3.1.9"
|
||||||
|
description = "Simple construction, analysis and modification of binary data."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "22.3.0"
|
||||||
|
description = "The uncompromising code formatter."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.2"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=8.0.0"
|
||||||
|
mypy-extensions = ">=0.4.3"
|
||||||
|
pathspec = ">=0.9.0"
|
||||||
|
platformdirs = ">=2"
|
||||||
|
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||||
|
typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
|
||||||
|
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
|
d = ["aiohttp (>=3.7.4)"]
|
||||||
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2022.5.18.1"
|
||||||
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "2.0.12"
|
||||||
|
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
unicode_backport = ["unicodedata2"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.3"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.4"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "docker"
|
||||||
|
version = "5.0.3"
|
||||||
|
description = "A Python library for the Docker Engine API."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pywin32 = {version = "227", markers = "sys_platform == \"win32\""}
|
||||||
|
requests = ">=2.14.2,<2.18.0 || >2.18.0"
|
||||||
|
websocket-client = ">=0.32.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
ssh = ["paramiko (>=2.4.2)"]
|
||||||
|
tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=3.4.7)", "idna (>=2.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "frozenlist"
|
||||||
|
version = "1.3.0"
|
||||||
|
description = "A list-like structure which implements collections.abc.MutableSequence"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.3"
|
||||||
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "4.11.4"
|
||||||
|
description = "Read metadata from Python packages"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||||
|
zipp = ">=0.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
|
||||||
|
perf = ["ipython"]
|
||||||
|
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isort"
|
||||||
|
version = "5.10.1"
|
||||||
|
description = "A Python utility / library to sort Python imports."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1,<4.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
|
||||||
|
requirements_deprecated_finder = ["pipreqs", "pip-api"]
|
||||||
|
colors = ["colorama (>=0.4.3,<0.5.0)"]
|
||||||
|
plugins = ["setuptools"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lxml"
|
||||||
|
version = "4.8.0"
|
||||||
|
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cssselect = ["cssselect (>=0.7)"]
|
||||||
|
html5 = ["html5lib"]
|
||||||
|
htmlsoup = ["beautifulsoup4"]
|
||||||
|
source = ["Cython (>=0.29.7)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multidict"
|
||||||
|
version = "6.0.2"
|
||||||
|
description = "multidict implementation"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "0.4.3"
|
||||||
|
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mysqlclient"
|
||||||
|
version = "2.1.0"
|
||||||
|
description = "Python interface to MySQL"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "0.9.0"
|
||||||
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "peewee"
|
||||||
|
version = "3.14.10"
|
||||||
|
description = "a little orm"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "2.5.2"
|
||||||
|
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
|
||||||
|
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pywin32"
|
||||||
|
version = "227"
|
||||||
|
description = "Python for Window Extensions"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.27.1"
|
||||||
|
description = "Python HTTP for Humans."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = ">=2017.4.17"
|
||||||
|
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
|
||||||
|
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
|
||||||
|
urllib3 = ">=1.21.1,<1.27"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
|
||||||
|
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruamel.yaml"
|
||||||
|
version = "0.17.21"
|
||||||
|
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["ryd"]
|
||||||
|
jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruamel.yaml.clib"
|
||||||
|
version = "0.2.6"
|
||||||
|
description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.0.1"
|
||||||
|
description = "A lil' TOML parser"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-ast"
|
||||||
|
version = "1.5.4"
|
||||||
|
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.2.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "udm-rest-client"
|
||||||
|
version = "1.0.7"
|
||||||
|
description = "Python library to interact with the Univention UDM REST API. Implements the simple Python UDM API."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
aiohttp = ">=3.8.1,<4"
|
||||||
|
async-property = ">=0.2.1,<0.3"
|
||||||
|
click = ">=7,<9"
|
||||||
|
docker = ">=5.0.3,<6"
|
||||||
|
requests = ">=2.26,<3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "1.26.9"
|
||||||
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
|
||||||
|
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "websocket-client"
|
||||||
|
version = "1.3.2"
|
||||||
|
description = "WebSocket client for Python with low level API options"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"]
|
||||||
|
optional = ["python-socks", "wsaccel"]
|
||||||
|
test = ["websockets"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yarl"
|
||||||
|
version = "1.7.2"
|
||||||
|
description = "Yet another URL library"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
idna = ">=2.0"
|
||||||
|
multidict = ">=4.0"
|
||||||
|
typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.8.0"
|
||||||
|
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
|
||||||
|
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "1.1"
|
||||||
|
python-versions = "^3.7"
|
||||||
|
content-hash = "ad657c4c736847ce55f3049ce04eb4a8c502914e083004842ae1f4091dd44c66"
|
||||||
|
|
||||||
|
[metadata.files]
|
||||||
|
aiohttp = [
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"},
|
||||||
|
{file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"},
|
||||||
|
{file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"},
|
||||||
|
{file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"},
|
||||||
|
{file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"},
|
||||||
|
{file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"},
|
||||||
|
{file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"},
|
||||||
|
]
|
||||||
|
aiosignal = [
|
||||||
|
{file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"},
|
||||||
|
{file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"},
|
||||||
|
]
|
||||||
|
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-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
|
||||||
|
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
|
||||||
|
]
|
||||||
|
asynctest = [
|
||||||
|
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
|
||||||
|
{file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
|
||||||
|
]
|
||||||
|
attrs = [
|
||||||
|
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
|
||||||
|
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
|
||||||
|
]
|
||||||
|
bitstring = [
|
||||||
|
{file = "bitstring-3.1.9-py2-none-any.whl", hash = "sha256:e3e340e58900a948787a05e8c08772f1ccbe133f6f41fe3f0fa19a18a22bbf4f"},
|
||||||
|
{file = "bitstring-3.1.9-py3-none-any.whl", hash = "sha256:0de167daa6a00c9386255a7cac931b45e6e24e0ad7ea64f1f92a64ac23ad4578"},
|
||||||
|
{file = "bitstring-3.1.9.tar.gz", hash = "sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7"},
|
||||||
|
]
|
||||||
|
black = [
|
||||||
|
{file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"},
|
||||||
|
{file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"},
|
||||||
|
{file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"},
|
||||||
|
{file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"},
|
||||||
|
{file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"},
|
||||||
|
{file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"},
|
||||||
|
{file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"},
|
||||||
|
{file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"},
|
||||||
|
{file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"},
|
||||||
|
{file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"},
|
||||||
|
{file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"},
|
||||||
|
{file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"},
|
||||||
|
{file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"},
|
||||||
|
{file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"},
|
||||||
|
{file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"},
|
||||||
|
{file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"},
|
||||||
|
{file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"},
|
||||||
|
{file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"},
|
||||||
|
{file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"},
|
||||||
|
{file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"},
|
||||||
|
{file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"},
|
||||||
|
{file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"},
|
||||||
|
{file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"},
|
||||||
|
]
|
||||||
|
certifi = [
|
||||||
|
{file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"},
|
||||||
|
{file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"},
|
||||||
|
]
|
||||||
|
charset-normalizer = [
|
||||||
|
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
|
||||||
|
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
|
||||||
|
]
|
||||||
|
click = [
|
||||||
|
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||||
|
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||||
|
]
|
||||||
|
colorama = [
|
||||||
|
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||||
|
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||||
|
]
|
||||||
|
docker = [
|
||||||
|
{file = "docker-5.0.3-py2.py3-none-any.whl", hash = "sha256:7a79bb439e3df59d0a72621775d600bc8bc8b422d285824cb37103eab91d1ce0"},
|
||||||
|
{file = "docker-5.0.3.tar.gz", hash = "sha256:d916a26b62970e7c2f554110ed6af04c7ccff8e9f81ad17d0d40c75637e227fb"},
|
||||||
|
]
|
||||||
|
frozenlist = [
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"},
|
||||||
|
{file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"},
|
||||||
|
{file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"},
|
||||||
|
{file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"},
|
||||||
|
{file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"},
|
||||||
|
{file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"},
|
||||||
|
]
|
||||||
|
idna = [
|
||||||
|
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||||
|
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
|
||||||
|
]
|
||||||
|
importlib-metadata = [
|
||||||
|
{file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"},
|
||||||
|
{file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"},
|
||||||
|
]
|
||||||
|
isort = [
|
||||||
|
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
|
||||||
|
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
|
||||||
|
]
|
||||||
|
lxml = [
|
||||||
|
{file = "lxml-4.8.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b"},
|
||||||
|
{file = "lxml-4.8.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430"},
|
||||||
|
{file = "lxml-4.8.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a"},
|
||||||
|
{file = "lxml-4.8.0-cp27-cp27m-win32.whl", hash = "sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5"},
|
||||||
|
{file = "lxml-4.8.0-cp27-cp27m-win_amd64.whl", hash = "sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9"},
|
||||||
|
{file = "lxml-4.8.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc"},
|
||||||
|
{file = "lxml-4.8.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170"},
|
||||||
|
{file = "lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9"},
|
||||||
|
{file = "lxml-4.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03"},
|
||||||
|
{file = "lxml-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe"},
|
||||||
|
{file = "lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa"},
|
||||||
|
{file = "lxml-4.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1"},
|
||||||
|
{file = "lxml-4.8.0-cp310-cp310-win32.whl", hash = "sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b"},
|
||||||
|
{file = "lxml-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76"},
|
||||||
|
{file = "lxml-4.8.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6"},
|
||||||
|
{file = "lxml-4.8.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2"},
|
||||||
|
{file = "lxml-4.8.0-cp35-cp35m-win32.whl", hash = "sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150"},
|
||||||
|
{file = "lxml-4.8.0-cp35-cp35m-win_amd64.whl", hash = "sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654"},
|
||||||
|
{file = "lxml-4.8.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169"},
|
||||||
|
{file = "lxml-4.8.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb"},
|
||||||
|
{file = "lxml-4.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3"},
|
||||||
|
{file = "lxml-4.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4"},
|
||||||
|
{file = "lxml-4.8.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e"},
|
||||||
|
{file = "lxml-4.8.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613"},
|
||||||
|
{file = "lxml-4.8.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33"},
|
||||||
|
{file = "lxml-4.8.0-cp36-cp36m-win32.whl", hash = "sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429"},
|
||||||
|
{file = "lxml-4.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63"},
|
||||||
|
{file = "lxml-4.8.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a"},
|
||||||
|
{file = "lxml-4.8.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4"},
|
||||||
|
{file = "lxml-4.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15"},
|
||||||
|
{file = "lxml-4.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f"},
|
||||||
|
{file = "lxml-4.8.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c"},
|
||||||
|
{file = "lxml-4.8.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85"},
|
||||||
|
{file = "lxml-4.8.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141"},
|
||||||
|
{file = "lxml-4.8.0-cp37-cp37m-win32.whl", hash = "sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63"},
|
||||||
|
{file = "lxml-4.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8"},
|
||||||
|
{file = "lxml-4.8.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7"},
|
||||||
|
{file = "lxml-4.8.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428"},
|
||||||
|
{file = "lxml-4.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5"},
|
||||||
|
{file = "lxml-4.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f"},
|
||||||
|
{file = "lxml-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870"},
|
||||||
|
{file = "lxml-4.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9"},
|
||||||
|
{file = "lxml-4.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68"},
|
||||||
|
{file = "lxml-4.8.0-cp38-cp38-win32.whl", hash = "sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696"},
|
||||||
|
{file = "lxml-4.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939"},
|
||||||
|
{file = "lxml-4.8.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807"},
|
||||||
|
{file = "lxml-4.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1"},
|
||||||
|
{file = "lxml-4.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939"},
|
||||||
|
{file = "lxml-4.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca"},
|
||||||
|
{file = "lxml-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c"},
|
||||||
|
{file = "lxml-4.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87"},
|
||||||
|
{file = "lxml-4.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9"},
|
||||||
|
{file = "lxml-4.8.0-cp39-cp39-win32.whl", hash = "sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea"},
|
||||||
|
{file = "lxml-4.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c"},
|
||||||
|
{file = "lxml-4.8.0-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507"},
|
||||||
|
{file = "lxml-4.8.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9"},
|
||||||
|
{file = "lxml-4.8.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e"},
|
||||||
|
{file = "lxml-4.8.0-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0"},
|
||||||
|
{file = "lxml-4.8.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79"},
|
||||||
|
{file = "lxml-4.8.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93"},
|
||||||
|
{file = "lxml-4.8.0.tar.gz", hash = "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23"},
|
||||||
|
]
|
||||||
|
multidict = [
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"},
|
||||||
|
{file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"},
|
||||||
|
{file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"},
|
||||||
|
{file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"},
|
||||||
|
{file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"},
|
||||||
|
{file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"},
|
||||||
|
]
|
||||||
|
mypy-extensions = [
|
||||||
|
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||||
|
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||||
|
]
|
||||||
|
mysqlclient = [
|
||||||
|
{file = "mysqlclient-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:02c8826e6add9b20f4cb12dcf016485f7b1d6e30356a1204d05431867a1b3947"},
|
||||||
|
{file = "mysqlclient-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b62d23c11c516cedb887377c8807628c1c65d57593b57853186a6ee18b0c6a5b"},
|
||||||
|
{file = "mysqlclient-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:2c8410f54492a3d2488a6a53e2d85b7e016751a1e7d116e7aea9c763f59f5e8c"},
|
||||||
|
{file = "mysqlclient-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14"},
|
||||||
|
{file = "mysqlclient-2.1.0.tar.gz", hash = "sha256:973235686f1b720536d417bf0a0d39b4ab3d5086b2b6ad5e6752393428c02b12"},
|
||||||
|
]
|
||||||
|
pathspec = [
|
||||||
|
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||||
|
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
||||||
|
]
|
||||||
|
peewee = [
|
||||||
|
{file = "peewee-3.14.10.tar.gz", hash = "sha256:23271422b332c82d30c92597dee905ee831b56c6d99c33e05901e6891c75fe15"},
|
||||||
|
]
|
||||||
|
platformdirs = [
|
||||||
|
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||||
|
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
||||||
|
]
|
||||||
|
pywin32 = [
|
||||||
|
{file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"},
|
||||||
|
{file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"},
|
||||||
|
{file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"},
|
||||||
|
{file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"},
|
||||||
|
{file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"},
|
||||||
|
{file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"},
|
||||||
|
{file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"},
|
||||||
|
{file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"},
|
||||||
|
{file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"},
|
||||||
|
{file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"},
|
||||||
|
{file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"},
|
||||||
|
{file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"},
|
||||||
|
]
|
||||||
|
requests = [
|
||||||
|
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
|
||||||
|
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
|
||||||
|
]
|
||||||
|
"ruamel.yaml" = [
|
||||||
|
{file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"},
|
||||||
|
{file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"},
|
||||||
|
]
|
||||||
|
"ruamel.yaml.clib" = [
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"},
|
||||||
|
{file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"},
|
||||||
|
]
|
||||||
|
tomli = [
|
||||||
|
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||||
|
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||||
|
]
|
||||||
|
typed-ast = [
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"},
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"},
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"},
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"},
|
||||||
|
{file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"},
|
||||||
|
{file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"},
|
||||||
|
{file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"},
|
||||||
|
{file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"},
|
||||||
|
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
|
||||||
|
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
|
||||||
|
]
|
||||||
|
typing-extensions = [
|
||||||
|
{file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
|
||||||
|
{file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
|
||||||
|
]
|
||||||
|
udm-rest-client = [
|
||||||
|
{file = "udm-rest-client-1.0.7.tar.gz", hash = "sha256:b0468cd16a5ec3c8f1fb2da70c7db6437b2283fdfdb64cb0b21e36ad59a8ad98"},
|
||||||
|
{file = "udm_rest_client-1.0.7-py2.py3-none-any.whl", hash = "sha256:6ecf462fa5d48c11010e48c340270c95b99ad3ba4a571d02c3fcc0471339e35f"},
|
||||||
|
]
|
||||||
|
urllib3 = [
|
||||||
|
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
|
||||||
|
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
|
||||||
|
]
|
||||||
|
websocket-client = [
|
||||||
|
{file = "websocket-client-1.3.2.tar.gz", hash = "sha256:50b21db0058f7a953d67cc0445be4b948d7fc196ecbeb8083d68d94628e4abf6"},
|
||||||
|
{file = "websocket_client-1.3.2-py3-none-any.whl", hash = "sha256:722b171be00f2b90e1d4fb2f2b53146a536ca38db1da8ff49c972a4e1365d0ef"},
|
||||||
|
]
|
||||||
|
yarl = [
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"},
|
||||||
|
{file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"},
|
||||||
|
{file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"},
|
||||||
|
{file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"},
|
||||||
|
{file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"},
|
||||||
|
{file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"},
|
||||||
|
{file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"},
|
||||||
|
]
|
||||||
|
zipp = [
|
||||||
|
{file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"},
|
||||||
|
{file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},
|
||||||
|
]
|
40
pyproject.toml
Normal file
40
pyproject.toml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[tool.black]
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma = true
|
||||||
|
force_grid_wrap = 0
|
||||||
|
use_parentheses = true
|
||||||
|
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.17.20"
|
||||||
|
bitstring = "^3.1.6"
|
||||||
|
lxml = "^4.5.0"
|
||||||
|
peewee = "^3.13.2"
|
||||||
|
mysqlclient = "^2.1.0"
|
||||||
|
udm-rest-client = "^1.0.6"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
black = "^22.3.0"
|
||||||
|
isort = "^5.10.1"
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
doorUpdater = 'memberPlumbing.doorUpdater:main'
|
||||||
|
hidEvents = 'memberPlumbing.hidEvents:main'
|
||||||
|
sqlExport = 'memberPlumbing.sqlExport:main'
|
||||||
|
ucsAccounts = 'memberPlumbing.ucsAccounts:main'
|
||||||
|
upcomingEvents = 'memberPlumbing.upcomingEvents:main'
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry>=0.12"]
|
||||||
|
build-backend = "poetry.masonry.api"
|
@ -1,65 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<VertXMessage xmlns:hid="http://www.hidglobal.com/VertX">
|
|
||||||
<hid:Schedules action="AD">
|
|
||||||
<hid:Schedule scheduleID="1" scheduleName="7x24">
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="0" startTime="00:00:00" endTime="23:59:59"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="1" startTime="00:00:00" endTime="23:59:59"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="2" startTime="00:00:00" endTime="23:59:59"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="3" startTime="00:00:00" endTime="23:59:59"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="4" startTime="00:00:00" endTime="23:59:59"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="5" startTime="00:00:00" endTime="23:59:59"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="6" startTime="00:00:00" endTime="23:59:59"/>
|
|
||||||
</hid:Schedule>
|
|
||||||
|
|
||||||
<hid:Schedule scheduleID="2" scheduleName="Weekends Only">
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="0" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="6" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:SpecialDayInterval dayOfMonth="1" month="1" specialDayID="3" specialDayName="New Years Day" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:SpecialDayInterval dayOfMonth="25" month="12" specialDayID="2" specialDayName="Christmas Day" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
</hid:Schedule>
|
|
||||||
|
|
||||||
<hid:Schedule scheduleID="3" scheduleName="Weekdays Only">
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="1" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="2" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="3" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="4" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="5" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
</hid:Schedule>
|
|
||||||
|
|
||||||
<hid:Schedule scheduleID="4" scheduleName="Unlimited">
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="0" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="1" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="2" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="3" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="4" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="5" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="6" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
</hid:Schedule>
|
|
||||||
|
|
||||||
<hid:Schedule scheduleID="5" scheduleName="Nights and Weekends">
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="0" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="1" startTime="15:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="2" startTime="15:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="3" startTime="15:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="4" startTime="15:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="5" startTime="15:00:00" endTime="21:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="6" startTime="08:00:00" endTime="21:00:00"/>
|
|
||||||
</hid:Schedule>
|
|
||||||
|
|
||||||
<hid:Schedule scheduleID="6" scheduleName="Front Door Unlocker">
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="0" startTime="08:00:00" endTime="17:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="1" startTime="08:00:00" endTime="17:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="2" startTime="08:00:00" endTime="17:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="3" startTime="08:00:00" endTime="17:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="4" startTime="08:00:00" endTime="17:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="5" startTime="08:00:00" endTime="17:00:00"/>
|
|
||||||
<hid:DayOfWeekInterval dayOfWeek="6" startTime="08:00:00" endTime="17:00:00"/>
|
|
||||||
</hid:Schedule>
|
|
||||||
</hid:Schedules>
|
|
||||||
|
|
||||||
<hid:SpecialDays action="UD">
|
|
||||||
<hid:SpecialDay specialDayID="1" specialDayName="New Years Day" dayOfMonth="1" month="1"/>
|
|
||||||
<hid:SpecialDay specialDayID="2" specialDayName="Independance Day" dayOfMonth="4" month="7"/>
|
|
||||||
<hid:SpecialDay specialDayID="3" specialDayName="Christmas Day" dayOfMonth="25" month="12"/>
|
|
||||||
</hid:SpecialDays>
|
|
||||||
</VertXMessage>
|
|
10
systemd/doorUpdater.service
Normal file
10
systemd/doorUpdater.service
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Update HID door controls
|
||||||
|
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 doorUpdater
|
9
systemd/doorUpdater.timer
Normal file
9
systemd/doorUpdater.timer
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Hourly HID door control update
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*:0/15
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
12
systemd/hidEvents.service
Normal file
12
systemd/hidEvents.service
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Pull events from door controllers into local database
|
||||||
|
OnFailure=status-email-admin@%n.service
|
||||||
|
After=mariadb.service
|
||||||
|
Requires=mariadb.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=adam
|
||||||
|
Type=oneshot
|
||||||
|
TimeoutStartSec=600
|
||||||
|
WorkingDirectory=/home/adam/memberPlumbing/
|
||||||
|
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run hidEvents
|
9
systemd/hidEvents.timer
Normal file
9
systemd/hidEvents.timer
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Hourly door controller events sync
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=hourly
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
12
systemd/membershipworksSQL.service
Normal file
12
systemd/membershipworksSQL.service
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Sync Membershipworks with local database
|
||||||
|
OnFailure=status-email-admin@%n.service
|
||||||
|
After=mariadb.service
|
||||||
|
Requires=mariadb.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=adam
|
||||||
|
Type=oneshot
|
||||||
|
TimeoutStartSec=600
|
||||||
|
WorkingDirectory=/home/adam/memberPlumbing/
|
||||||
|
ExecStart=/usr/bin/python3 -u /home/adam/.poetry/bin/poetry run sqlExport
|
9
systemd/membershipworksSQL.timer
Normal file
9
systemd/membershipworksSQL.timer
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Hourly Membershipworks database sync
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=hourly
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
8
systemd/status-email-admin@.service
Normal file
8
systemd/status-email-admin@.service
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=status email for %i to sysadmin addresses
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/systemd-email "cms-errors@adamgoldsmith.name, steve@stevegoldsmith.com" %i
|
||||||
|
User=nobody
|
||||||
|
Group=systemd-journal
|
11
systemd/systemd-email
Executable file
11
systemd/systemd-email
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
/usr/sbin/sendmail -t <<ERRMAIL
|
||||||
|
To: $1
|
||||||
|
From: systemd <root@$HOSTNAME>
|
||||||
|
Subject: $2 failed on $(hostname)
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
|
||||||
|
$(systemctl status --full "$2")
|
||||||
|
ERRMAIL
|
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
|
@ -1,54 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title> CMS Members </title>
|
|
||||||
<style>
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
td, th{
|
|
||||||
border: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:nth-child(even) {
|
|
||||||
background-color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr[onHold=Yes] {
|
|
||||||
background-color: #f66;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<form action="./frontDoor/lock" method="post">
|
|
||||||
<button>Lock Front Door</button>
|
|
||||||
<button formaction="./frontDoor/unlock">Unlock Front Door</button>
|
|
||||||
</form>
|
|
||||||
<form>
|
|
||||||
<input type="text" name="term" />
|
|
||||||
<button>Search</button>
|
|
||||||
</form>
|
|
||||||
{% if headers is not none %}
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
{% for header in headers %}
|
|
||||||
<th> {{ header }} </th>
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
{% for member in members %}
|
|
||||||
<tr onHold ="{{ member['Account on Hold'] }}">
|
|
||||||
{% for header in headers %}
|
|
||||||
<td> {{ member[header] }} </td>
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</table>
|
|
||||||
{% endif %}
|
|
||||||
<span class="error">{{ error }}</span>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,89 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import string
|
|
||||||
|
|
||||||
from common import *
|
|
||||||
|
|
||||||
LDAP_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
|
||||||
GROUP_BASE = "cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
|
||||||
GROUPS_REGEX = "|".join(['Certified: .*', 'Access .*\?', 'CMS .*', 'Volunteer: .*'])
|
|
||||||
RAND_PW_LEN = 20
|
|
||||||
|
|
||||||
def makeGroups(members):
|
|
||||||
groups = [key.replace(':', '.').replace('?', '')
|
|
||||||
for key in members[0].keys()
|
|
||||||
if re.match(GROUPS_REGEX, key) is not None]
|
|
||||||
for group in groups:
|
|
||||||
subprocess.call(["udm", "groups/group", "create",
|
|
||||||
"--position", GROUP_BASE,
|
|
||||||
"--set", "name=" + group])
|
|
||||||
|
|
||||||
def makeSets(props):
|
|
||||||
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():
|
|
||||||
members = getMembershipworksData(['members', 'staff'],
|
|
||||||
"lvl,phn,eml,lbl,nam,end,_id")
|
|
||||||
makeGroups(members)
|
|
||||||
|
|
||||||
for member in members:
|
|
||||||
randomPass = ''.join(random.choice(string.ascii_letters + string.digits)
|
|
||||||
for x in range(0, RAND_PW_LEN))
|
|
||||||
username = member["Account Name"].lower().replace(" ", ".")
|
|
||||||
|
|
||||||
props = {
|
|
||||||
"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(["udm", "users/user", "create",
|
|
||||||
"--position", LDAP_BASE] + makeSets(props))
|
|
||||||
|
|
||||||
# remove props we don't want to reset
|
|
||||||
props.pop("password")
|
|
||||||
props.pop("pwdChangeNextLogin")
|
|
||||||
|
|
||||||
subprocess.call(["udm", "users/user", "modify",
|
|
||||||
"--dn", "uid=" + username + "," + LDAP_BASE]
|
|
||||||
+ makeSets(props)
|
|
||||||
+ makeAppendGroups(member))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
Loading…
Reference in New Issue
Block a user