forked from CMS/memberPlumbing
Setup poetry, apply Black and isort styling
This commit is contained in:
parent
525fd24a22
commit
7981a05a46
30
common.py
30
common.py
@ -1,11 +1,15 @@
|
||||
from ruamel.yaml import YAML
|
||||
import os
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
from lib.hid.DoorController import DoorController
|
||||
from lib.MembershipWorks import MembershipWorks
|
||||
|
||||
from passwords import DOOR_USERNAME, DOOR_PASSWORD
|
||||
from passwords import MEMBERSHIPWORKS_USERNAME, MEMBERSHIPWORKS_PASSWORD
|
||||
from passwords import (
|
||||
DOOR_PASSWORD,
|
||||
DOOR_USERNAME,
|
||||
MEMBERSHIPWORKS_PASSWORD,
|
||||
MEMBERSHIPWORKS_USERNAME,
|
||||
)
|
||||
|
||||
try:
|
||||
with open(os.path.dirname(os.path.abspath(__file__)) + "/config.yaml") as f:
|
||||
@ -14,13 +18,19 @@ except NameError:
|
||||
with open("config.yaml") as f:
|
||||
config = YAML().load(f)
|
||||
|
||||
doors = {doorName: DoorController(doorData['ip'],
|
||||
DOOR_USERNAME, DOOR_PASSWORD,
|
||||
name=doorName, access=doorData['access'])
|
||||
for doorName, doorData in config["doors"].items()}
|
||||
doors = {
|
||||
doorName: DoorController(
|
||||
doorData["ip"],
|
||||
DOOR_USERNAME,
|
||||
DOOR_PASSWORD,
|
||||
name=doorName,
|
||||
access=doorData["access"],
|
||||
)
|
||||
for doorName, doorData in config["doors"].items()
|
||||
}
|
||||
|
||||
memberLevels = config['memberLevels']
|
||||
doorSpecificSchedules = config['doorSpecificSchedules']
|
||||
memberLevels = config["memberLevels"]
|
||||
doorSpecificSchedules = config["doorSpecificSchedules"]
|
||||
|
||||
membershipworks = MembershipWorks()
|
||||
membershipworks.login(MEMBERSHIPWORKS_USERNAME, MEMBERSHIPWORKS_PASSWORD)
|
||||
|
310
doorUpdater.py
310
doorUpdater.py
@ -2,15 +2,27 @@
|
||||
|
||||
import copy
|
||||
|
||||
from common import doors, membershipworks, memberLevels, doorSpecificSchedules
|
||||
from common import doors, doorSpecificSchedules, memberLevels, membershipworks
|
||||
from lib.hid.Credential import Credential
|
||||
from lib.hid.DoorController import ROOT, E
|
||||
|
||||
class Member():
|
||||
def __init__(self, forename="", surname="", membershipWorksID="",
|
||||
middleName="", email="", phone="",
|
||||
cardholderID=None, doorAccess=[],
|
||||
credentials=set(), levels=[], extraLevels=[], schedules=[]):
|
||||
|
||||
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
|
||||
@ -38,68 +50,87 @@ Schedules: {self.schedules}
|
||||
|
||||
class MembershipworksMember(Member):
|
||||
def __init__(self, data, formerMember=False):
|
||||
super().__init__(data["First Name"],
|
||||
data["Last Name"],
|
||||
membershipWorksID=data["Account ID"],
|
||||
email=data["Email"],
|
||||
phone=data["Phone"])
|
||||
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"]))])
|
||||
self.credentials = set(
|
||||
[
|
||||
Credential(
|
||||
code=(
|
||||
data["Access Card Facility Code"],
|
||||
data["Access Card Number"],
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
self.credentials = set()
|
||||
|
||||
self.onHold=data["Account on Hold"] != ""
|
||||
self.limitedOperations=data['Access Permitted During Limited Operations'] == "Y"
|
||||
self.formerMember=formerMember
|
||||
self.onHold = data["Account on Hold"] != ""
|
||||
self.limitedOperations = (
|
||||
data["Access Permitted During Limited Operations"] == "Y"
|
||||
)
|
||||
self.formerMember = formerMember
|
||||
|
||||
levels = {k: v for k, v in 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"),
|
||||
[])
|
||||
schedule: sum(
|
||||
(doors for prop, doors in props.items() if data[prop] == "Y"), []
|
||||
)
|
||||
for schedule, props in doorSpecificSchedules.items()
|
||||
}
|
||||
|
||||
self.doorAccess = [
|
||||
door for door, doorData in doors.items()
|
||||
door
|
||||
for door, doorData in 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]
|
||||
|
||||
if (door.name not in self.doorAccess
|
||||
if (
|
||||
door.name not in self.doorAccess
|
||||
or self.onHold
|
||||
or self.formerMember
|
||||
or not self.limitedOperations):
|
||||
or not self.limitedOperations
|
||||
):
|
||||
schedules = []
|
||||
else:
|
||||
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)
|
||||
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}
|
||||
return (
|
||||
super().__str__()
|
||||
+ f"""OnHold? {self.onHold}
|
||||
Limited Operations Access? {self.limitedOperations}
|
||||
Former Member? {self.formerMember}
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class DoorMember(Member):
|
||||
@ -109,65 +140,83 @@ class DoorMember(Member):
|
||||
|
||||
@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 = 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'))
|
||||
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')]
|
||||
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}
|
||||
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,
|
||||
E.Role(
|
||||
{
|
||||
"roleID": self.cardholderID,
|
||||
"scheduleID": schedulesMap[schedule],
|
||||
"resourceID": "0"})
|
||||
for schedule in self.schedules]
|
||||
"resourceID": "0",
|
||||
}
|
||||
)
|
||||
for schedule in self.schedules
|
||||
]
|
||||
|
||||
return E.RoleSet({"action": "UD", "roleSetID": self.cardholderID},
|
||||
E.Roles(*roles))
|
||||
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]
|
||||
{
|
||||
"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()]}
|
||||
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())
|
||||
allCredentials = set(
|
||||
Credential(hex=c.attrib["rawCardNumber"]) for c in door.get_credentials()
|
||||
)
|
||||
|
||||
# TODO: can I combine requests?
|
||||
for membershipworksMember in members:
|
||||
@ -176,12 +225,12 @@ def update_door(door, members):
|
||||
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"]
|
||||
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
|
||||
@ -197,10 +246,14 @@ def update_door(door, members):
|
||||
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()))))
|
||||
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}")
|
||||
@ -210,69 +263,106 @@ def update_door(door, members):
|
||||
newCards = member.credentials
|
||||
|
||||
allNewCards = set(
|
||||
card for m in members if m != membershipworksMember
|
||||
for card in m.credentials)
|
||||
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": ""}))))
|
||||
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])
|
||||
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}))))
|
||||
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)))
|
||||
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}")
|
||||
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():
|
||||
membershipworks_attributes = \
|
||||
membershipworks_attributes = (
|
||||
"_id,nam,phn,eml,lvl,lbl,xws,xms,xsc,xas,xfd,xac,xcf,xeh,xse,xlo"
|
||||
)
|
||||
|
||||
memberData = membershipworks.get_members(
|
||||
['Members', 'CMS Staff', 'Misc. Access'], membershipworks_attributes)
|
||||
["Members", "CMS Staff", "Misc. Access"], membershipworks_attributes
|
||||
)
|
||||
members = [MembershipworksMember(m) for m in memberData]
|
||||
|
||||
formerMemberData = membershipworks.get_members(
|
||||
['Former Members'], membershipworks_attributes)
|
||||
formerMembers = [MembershipworksMember(m, formerMember=True)
|
||||
for m in formerMemberData]
|
||||
["Former Members"], membershipworks_attributes
|
||||
)
|
||||
formerMembers = [
|
||||
MembershipworksMember(m, formerMember=True) for m in formerMemberData
|
||||
]
|
||||
|
||||
for formerMember in formerMembers:
|
||||
member = next(
|
||||
(m for m in members
|
||||
if m.membershipWorksID == formerMember.membershipWorksID),
|
||||
None)
|
||||
(
|
||||
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
|
||||
else: # member is only a former member
|
||||
formerMember.formerMember = True
|
||||
members.append(formerMember)
|
||||
|
||||
@ -281,5 +371,5 @@ def main():
|
||||
update_door(door, members)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
59
events.py
59
events.py
@ -1,27 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
from collections import defaultdict
|
||||
from lxml import etree
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
|
||||
from common import *
|
||||
import requests
|
||||
from lxml import etree
|
||||
|
||||
from common import doors
|
||||
from lib.hid.DoorController import E, E_plain
|
||||
|
||||
|
||||
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)
|
||||
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')]
|
||||
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"}))
|
||||
parXMLIn = E_plain.VertXMessage(E.EventMessages({"action": "LR"}))
|
||||
parXMLOut = door.doXMLRequest(parXMLIn)
|
||||
etree.dump(parXMLOut)
|
||||
|
||||
@ -29,8 +36,9 @@ def getMessages(door):
|
||||
# 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"])
|
||||
recordCount = int(parXMLOut[0].attrib["historyRecordMarker"]) - int(
|
||||
root[0][0].attrib["recordMarker"]
|
||||
)
|
||||
else:
|
||||
# first run for this door
|
||||
root = None
|
||||
@ -42,26 +50,35 @@ def getMessages(door):
|
||||
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"]}))
|
||||
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
|
||||
# TODO: handle modeRecords=true
|
||||
|
||||
for index, event in enumerate(eventsXMLOut[0]):
|
||||
event.attrib["recordMarker"] = str(int(parXMLOut[0].attrib["historyRecordMarker"]) - index)
|
||||
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")
|
||||
tree.write("logs/" + door.name + ".xml")
|
||||
|
||||
|
||||
def main():
|
||||
for door in doors.values():
|
||||
getMessages(door)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
@ -1,44 +1,48 @@
|
||||
import csv
|
||||
from io import StringIO
|
||||
|
||||
import requests
|
||||
|
||||
BASE_URL = "https://api.membershipworks.com/v1/"
|
||||
|
||||
# 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"}
|
||||
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
|
||||
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"}
|
||||
12: "Required waiver/terms",
|
||||
}
|
||||
|
||||
# more constants, this time extracted from the members csv export in all.js
|
||||
staticFlags = {
|
||||
@ -55,14 +59,16 @@ staticFlags = {
|
||||
"spy": {"lbl": "Billing method"},
|
||||
"rid": {"lbl": "Auto recurring billing ID"},
|
||||
"ipa": {"lbl": "IP address"},
|
||||
"_id": {"lbl": "Account ID"}
|
||||
"_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}")
|
||||
f"Error when attempting {reason}: {r.status_code} {r.reason}\n{r.text}"
|
||||
)
|
||||
|
||||
|
||||
class MembershipWorks:
|
||||
def __init__(self):
|
||||
@ -72,25 +78,24 @@ class MembershipWorks:
|
||||
|
||||
def login(self, username, password):
|
||||
"""Authenticate against the membershipworks api"""
|
||||
r = requests.post(BASE_URL + 'usr',
|
||||
data={"_st": "all",
|
||||
"eml": username,
|
||||
"org": "10000",
|
||||
"pwd": password})
|
||||
if r.status_code != 200 or 'SF' not in r.json():
|
||||
raise MembershipWorksRemoteError('login', r)
|
||||
r = requests.post(
|
||||
BASE_URL + "usr",
|
||||
data={"_st": "all", "eml": username, "org": "10000", "pwd": password},
|
||||
)
|
||||
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.auth_token = self.org_info["SF"]
|
||||
self.org_num = self.org_info["org"]
|
||||
|
||||
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')
|
||||
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
|
||||
if "params" not in kwargs:
|
||||
kwargs["params"] = {}
|
||||
kwargs["params"]["SF"] = self.auth_token
|
||||
|
||||
def _get(self, *args, **kwargs):
|
||||
self._inject_auth(kwargs)
|
||||
@ -115,14 +120,14 @@ class MembershipWorks:
|
||||
# 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
|
||||
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
|
||||
|
||||
@ -131,36 +136,37 @@ class MembershipWorks:
|
||||
|
||||
This is terrible, and there might be a better way to do this.
|
||||
"""
|
||||
ret = {"folders": {},
|
||||
"levels": {},
|
||||
"addons": {},
|
||||
"labels": {}}
|
||||
ret = {"folders": {}, "levels": {}, "addons": {}, "labels": {}}
|
||||
|
||||
for dek in self.org_info['dek']:
|
||||
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['_id']
|
||||
elif 'cur' in dek:
|
||||
ret["levels"][dek['lbl']] = dek['_id']
|
||||
elif 'mux' in dek:
|
||||
ret["addons"][dek['lbl']] = dek['_id']
|
||||
if dek["dek"] == 1:
|
||||
ret["folders"][dek["lbl"]] = dek["_id"]
|
||||
elif "cur" in dek:
|
||||
ret["levels"][dek["lbl"]] = dek["_id"]
|
||||
elif "mux" in dek:
|
||||
ret["addons"][dek["lbl"]] = dek["_id"]
|
||||
else:
|
||||
ret["labels"][dek['lbl']] = dek['_id']
|
||||
ret["labels"][dek["lbl"]] = dek["_id"]
|
||||
|
||||
return ret
|
||||
|
||||
def get_member_ids(self, folders):
|
||||
folder_map = self._parse_flags()["folders"]
|
||||
|
||||
r = self._get(BASE_URL + "ylp", params={
|
||||
"lbl": ",".join([folder_map[f] for f in folders]),
|
||||
"org": self.org_num,
|
||||
"var": "_id,nam,ctc"})
|
||||
if r.status_code != 200 or 'usr' not in r.json():
|
||||
raise MembershipWorksRemoteError('user listing', r)
|
||||
r = self._get(
|
||||
BASE_URL + "ylp",
|
||||
params={
|
||||
"lbl": ",".join([folder_map[f] for f in folders]),
|
||||
"org": self.org_num,
|
||||
"var": "_id,nam,ctc",
|
||||
},
|
||||
)
|
||||
if r.status_code != 200 or "usr" not in r.json():
|
||||
raise MembershipWorksRemoteError("user listing", r)
|
||||
|
||||
# get list of member ID matching the search
|
||||
return [user['uid'] for user in r.json()['usr']]
|
||||
return [user["uid"] for user in r.json()["usr"]]
|
||||
|
||||
# TODO: has issues with aliasing header names:
|
||||
# ex: "Personal Studio Space" Label vs Membership Addon/Field
|
||||
@ -173,13 +179,17 @@ class MembershipWorks:
|
||||
|
||||
# get members CSV
|
||||
# TODO: maybe can just use previous get instead? would return JSON
|
||||
r = self._post(BASE_URL + "csv",
|
||||
data={"_rt": "946702800", # unknown
|
||||
"mux": "", # unknown
|
||||
"tid": ",".join(ids), # ids of members to get
|
||||
"var": columns})
|
||||
r = self._post(
|
||||
BASE_URL + "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)
|
||||
raise MembershipWorksRemoteError("csv generation", r)
|
||||
return list(csv.DictReader(StringIO(r.text)))
|
||||
|
||||
def get_transactions(self, start_date, end_date, json=False):
|
||||
@ -190,13 +200,17 @@ class MembershipWorks:
|
||||
json gets a different version of the transactions list,
|
||||
which contains a different set information
|
||||
"""
|
||||
r = self._get(BASE_URL + "csv",
|
||||
params={'crm': '12,13,14,18,19', # transaction types, see CRM
|
||||
**({'txl': ''} if json else {}),
|
||||
'sdp': start_date.strftime('%s'),
|
||||
'edp': end_date.strftime('%s')})
|
||||
r = self._get(
|
||||
BASE_URL + "csv",
|
||||
params={
|
||||
"crm": "12,13,14,18,19", # transaction types, see CRM
|
||||
**({"txl": ""} if json else {}),
|
||||
"sdp": start_date.strftime("%s"),
|
||||
"edp": end_date.strftime("%s"),
|
||||
},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
raise MembershipWorksRemoteError('csv generation', r)
|
||||
raise MembershipWorksRemoteError("csv generation", r)
|
||||
if json:
|
||||
return r.json()
|
||||
else:
|
||||
|
@ -3,6 +3,7 @@ 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:
|
||||
@ -11,8 +12,10 @@ class Credential:
|
||||
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])
|
||||
"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:
|
||||
|
@ -1,30 +1,37 @@
|
||||
import csv
|
||||
from io import StringIO
|
||||
import urllib3
|
||||
|
||||
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"})
|
||||
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(",")
|
||||
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}")
|
||||
super().__init__(f"Door Updating Error: {r.status_code} {r.reason}\n{r.text}")
|
||||
|
||||
class DoorController():
|
||||
|
||||
class DoorController:
|
||||
def __init__(self, ip, username, password, name="", access=""):
|
||||
self.ip = ip
|
||||
self.username = username
|
||||
@ -35,59 +42,68 @@ class DoorController():
|
||||
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',
|
||||
"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
|
||||
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:
|
||||
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": "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},
|
||||
"https://" + self.ip + "/cgi-bin/vertx_xml.cgi",
|
||||
params={"XML": prefix + xml},
|
||||
auth=requests.auth.HTTPDigestAuth(self.username, self.password),
|
||||
verify=False)
|
||||
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:
|
||||
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]}
|
||||
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"})))
|
||||
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]))
|
||||
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
|
||||
@ -100,8 +116,11 @@ class DoorController():
|
||||
|
||||
# clear all schedules
|
||||
delXML = ROOT(
|
||||
*[E.Schedules({"action": "DD", "scheduleID": str(ii)})
|
||||
for ii in range(1, 8)])
|
||||
*[
|
||||
E.Schedules({"action": "DD", "scheduleID": str(ii)})
|
||||
for ii in range(1, 8)
|
||||
]
|
||||
)
|
||||
try:
|
||||
self.doXMLRequest(delXML)
|
||||
except RemoteError:
|
||||
@ -113,21 +132,27 @@ class DoorController():
|
||||
|
||||
def get_cardFormats(self):
|
||||
cardFormats = self.doXMLRequest(
|
||||
ROOT(E.CardFormats({"action": "LR",
|
||||
"responseFormat": "expanded"})))
|
||||
ROOT(E.CardFormats({"action": "LR", "responseFormat": "expanded"}))
|
||||
)
|
||||
|
||||
return {fmt[0].attrib["value"]: fmt.attrib["formatID"]
|
||||
for fmt in cardFormats[0].findall('{*}CardFormat[{*}FixedField]')}
|
||||
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)}))))
|
||||
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={}):
|
||||
@ -142,37 +167,41 @@ class DoorController():
|
||||
# poorly if the numbers line up poorly (ie an exact multiple
|
||||
# of the returned record limit)
|
||||
while moreRecords:
|
||||
res = self.doXMLRequest(ROOT(
|
||||
req({
|
||||
"action": "LR",
|
||||
"recordCount": str(count - recordCount + 1),
|
||||
"recordOffset": str(recordCount - 1
|
||||
if recordCount > 0 else 0),
|
||||
**params
|
||||
})))
|
||||
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'
|
||||
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"})
|
||||
return self.get_records(E.Cardholders, 1000, {"responseFormat": "expanded"})
|
||||
|
||||
def get_credentials(self):
|
||||
return self.get_records(E.Credentials, 1000)
|
||||
|
||||
def get_lock(self):
|
||||
el = ROOT(
|
||||
E.Doors({"action": "LR", "responseFormat": "status"}))
|
||||
el = ROOT(E.Doors({"action": "LR", "responseFormat": "status"}))
|
||||
xml = self.doXMLRequest(el)
|
||||
relayState = xml.find('./{*}Doors/{*}Door').attrib['relayState']
|
||||
relayState = xml.find("./{*}Doors/{*}Door").attrib["relayState"]
|
||||
return "unlocked" if relayState == "set" else "locked"
|
||||
|
||||
def set_lock(self, lock=True):
|
||||
el = ROOT(
|
||||
E.Doors({"action": "CM",
|
||||
"command": "lockDoor" if lock else "unlockDoor"}))
|
||||
E.Doors({"action": "CM", "command": "lockDoor" if lock else "unlockDoor"})
|
||||
)
|
||||
return self.doXMLRequest(el)
|
||||
|
265
lib/mw_models.py
265
lib/mw_models.py
@ -1,20 +1,26 @@
|
||||
from datetime import datetime
|
||||
|
||||
from peewee import (BooleanField, FixedCharField, CompositeKey, DateField,
|
||||
DateTimeField, DecimalField, ForeignKeyField, Model,
|
||||
MySQLDatabase, TextField)
|
||||
from peewee import (
|
||||
BooleanField,
|
||||
CompositeKey,
|
||||
DateField,
|
||||
DateTimeField,
|
||||
DecimalField,
|
||||
FixedCharField,
|
||||
ForeignKeyField,
|
||||
Model,
|
||||
MySQLDatabase,
|
||||
TextField,
|
||||
)
|
||||
|
||||
import passwords
|
||||
|
||||
database = MySQLDatabase(
|
||||
**passwords.MEMBERSHIPWORKS_DB,
|
||||
**{
|
||||
"charset": "utf8",
|
||||
"sql_mode": "PIPES_AS_CONCAT",
|
||||
"use_unicode": True,
|
||||
}
|
||||
**{"charset": "utf8", "sql_mode": "PIPES_AS_CONCAT", "use_unicode": True,}
|
||||
)
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
_csv_headers_override = {}
|
||||
_date_fields = {}
|
||||
@ -23,9 +29,9 @@ class BaseModel(Model):
|
||||
return self.insert(**self.__data__)
|
||||
|
||||
def upsert_instance(self):
|
||||
return self.insert_instance() \
|
||||
.on_conflict(action="update",
|
||||
preserve=list(self._meta.fields.values()))
|
||||
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:
|
||||
@ -35,8 +41,7 @@ class BaseModel(Model):
|
||||
|
||||
@classmethod
|
||||
def _headers_map(cls):
|
||||
return {field.column_name: name
|
||||
for name, field in cls._meta.fields.items()}
|
||||
return {field.column_name: name for name, field in cls._meta.fields.items()}
|
||||
|
||||
@classmethod
|
||||
def _remap_headers(cls, data):
|
||||
@ -64,140 +69,176 @@ class BaseModel(Model):
|
||||
class Meta:
|
||||
database = database
|
||||
|
||||
|
||||
class Label(BaseModel):
|
||||
label_id = FixedCharField(24, primary_key=True)
|
||||
label = TextField(null=True)
|
||||
|
||||
class Meta:
|
||||
table_name = 'labels'
|
||||
table_name = "labels"
|
||||
|
||||
|
||||
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?', null=True)
|
||||
access_front_door_and_studio_space_during_extended_hours = BooleanField(column_name='Access Front Door and Studio Space During Extended Hours?', null=True)
|
||||
access_wood_shop = BooleanField(column_name='Access Wood Shop?', null=True)
|
||||
access_metal_shop = BooleanField(column_name='Access Metal Shop?', null=True)
|
||||
access_storage_closet = BooleanField(column_name='Access Storage Closet?', null=True)
|
||||
access_studio_space = BooleanField(column_name='Access Studio Space?', null=True)
|
||||
access_front_door = BooleanField(column_name='Access Front Door?', null=True)
|
||||
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', null=True)
|
||||
membership_agreement_acknowledgement_page_filled_out = BooleanField(column_name='Membership Agreement Acknowledgement Page Filled Out', null=True)
|
||||
membership_agreement_signed = BooleanField(column_name='Membership Agreement Signed', null=True)
|
||||
liability_form_filled_out = BooleanField(column_name='Liability Form Filled Out', null=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?", null=True
|
||||
)
|
||||
access_front_door_and_studio_space_during_extended_hours = BooleanField(
|
||||
column_name="Access Front Door and Studio Space During Extended Hours?",
|
||||
null=True,
|
||||
)
|
||||
access_wood_shop = BooleanField(column_name="Access Wood Shop?", null=True)
|
||||
access_metal_shop = BooleanField(column_name="Access Metal Shop?", null=True)
|
||||
access_storage_closet = BooleanField(
|
||||
column_name="Access Storage Closet?", null=True
|
||||
)
|
||||
access_studio_space = BooleanField(column_name="Access Studio Space?", null=True)
|
||||
access_front_door = BooleanField(column_name="Access Front Door?", null=True)
|
||||
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", null=True
|
||||
)
|
||||
membership_agreement_acknowledgement_page_filled_out = BooleanField(
|
||||
column_name="Membership Agreement Acknowledgement Page Filled Out", null=True
|
||||
)
|
||||
membership_agreement_signed = BooleanField(
|
||||
column_name="Membership Agreement Signed", null=True
|
||||
)
|
||||
liability_form_filled_out = BooleanField(
|
||||
column_name="Liability Form Filled Out", null=True
|
||||
)
|
||||
|
||||
_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'
|
||||
"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",
|
||||
}
|
||||
|
||||
_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'
|
||||
"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'
|
||||
table_name = "members"
|
||||
|
||||
|
||||
class MemberLabel(BaseModel):
|
||||
uid = ForeignKeyField(Member, column_name='uid', backref='labels')
|
||||
label_id = ForeignKeyField(Label, backref='members')
|
||||
uid = ForeignKeyField(Member, column_name="uid", backref="labels")
|
||||
label_id = ForeignKeyField(Label, backref="members")
|
||||
|
||||
class Meta:
|
||||
table_name = 'member_labels'
|
||||
primary_key = CompositeKey('label_id', 'uid')
|
||||
table_name = "member_labels"
|
||||
primary_key = CompositeKey("label_id", "uid")
|
||||
|
||||
|
||||
class Transaction(BaseModel):
|
||||
sid = FixedCharField(27, null=True)
|
||||
uid = ForeignKeyField(Member, column_name='uid', backref='transactions', 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)
|
||||
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'
|
||||
}
|
||||
_csv_headers_override = {"_dp": "timestamp", "Transaction Type": "type"}
|
||||
|
||||
@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'])
|
||||
txn["_dp"] = datetime.fromtimestamp(txn["_dp"])
|
||||
return super().from_csv_dict(txn)
|
||||
|
||||
class Meta:
|
||||
table_name = 'transactions'
|
||||
table_name = "transactions"
|
||||
primary_key = False
|
||||
|
@ -1,70 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
import re
|
||||
import http
|
||||
import re
|
||||
|
||||
from flask import Flask, render_template, request
|
||||
|
||||
from common import doors, membershipworks
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
from common import doors, membershipworks
|
||||
|
||||
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 != '':
|
||||
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" }
|
||||
"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 (.*)')
|
||||
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', '')
|
||||
term = request.args.get("term", "")
|
||||
|
||||
if len(term) < 3:
|
||||
return render_template("members.html",
|
||||
error="Enter at least 3 characters to search")
|
||||
return render_template(
|
||||
"members.html", error="Enter at least 3 characters to search"
|
||||
)
|
||||
|
||||
data = membershipworks.get_members(
|
||||
['members', 'staff'],
|
||||
"lvl,xws,xms,xsc,xas,xfd,xac,phn,eml,lbl,xcf,nam,end")
|
||||
["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']
|
||||
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.")
|
||||
"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'])
|
||||
|
||||
@app.route("/frontDoor/<lock>", methods=["POST"])
|
||||
def unlockLockDoor(lock):
|
||||
doors['Front Door'].lockOrUnlockDoor(lock != 'unlock')
|
||||
return ('', http.HTTPStatus.NO_CONTENT)
|
||||
doors["Front Door"].lockOrUnlockDoor(lock != "unlock")
|
||||
return ("", http.HTTPStatus.NO_CONTENT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, host='0.0.0.0')
|
||||
app.run(debug=True, host="0.0.0.0")
|
||||
|
383
poetry.lock
generated
Normal file
383
poetry.lock
generated
Normal file
@ -0,0 +1,383 @@
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
name = "appdirs"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.4.3"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Classes Without Boilerplate"
|
||||
name = "attrs"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "19.3.0"
|
||||
|
||||
[package.extras]
|
||||
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
|
||||
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
|
||||
docs = ["sphinx", "zope.interface"]
|
||||
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Simple construction, analysis and modification of binary data."
|
||||
name = "bitstring"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "3.1.6"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "The uncompromising code formatter."
|
||||
name = "black"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
version = "19.10b0"
|
||||
|
||||
[package.dependencies]
|
||||
appdirs = "*"
|
||||
attrs = ">=18.1.0"
|
||||
click = ">=6.5"
|
||||
pathspec = ">=0.6,<1"
|
||||
regex = "*"
|
||||
toml = ">=0.9.4"
|
||||
typed-ast = ">=1.4.0"
|
||||
|
||||
[package.extras]
|
||||
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
name = "certifi"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2020.4.5.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Universal encoding detector for Python 2 and 3"
|
||||
name = "chardet"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "3.0.4"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Composable command line interface toolkit"
|
||||
name = "click"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "7.1.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
name = "idna"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "2.9"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
name = "isort"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "4.3.21"
|
||||
|
||||
[package.extras]
|
||||
pipfile = ["pipreqs", "requirementslib"]
|
||||
pyproject = ["toml"]
|
||||
requirements = ["pipreqs", "pip-api"]
|
||||
xdg_home = ["appdirs (>=1.4.0)"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||
name = "lxml"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
|
||||
version = "4.5.0"
|
||||
|
||||
[package.extras]
|
||||
cssselect = ["cssselect (>=0.7)"]
|
||||
html5 = ["html5lib"]
|
||||
htmlsoup = ["beautifulsoup4"]
|
||||
source = ["Cython (>=0.29.7)"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python interface to MySQL"
|
||||
name = "mysqlclient"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.4.6"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
name = "pathspec"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "0.8.0"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "a little orm"
|
||||
name = "peewee"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "3.13.3"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Alternative regular expression module, to replace re."
|
||||
name = "regex"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2020.4.4"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "Python HTTP for Humans."
|
||||
name = "requests"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "2.23.0"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
chardet = ">=3.0.2,<4"
|
||||
idna = ">=2.5,<3"
|
||||
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
|
||||
|
||||
[package.extras]
|
||||
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
|
||||
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order"
|
||||
name = "ruamel.yaml"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.16.10"
|
||||
|
||||
[package.dependencies]
|
||||
[package.dependencies."ruamel.yaml.clib"]
|
||||
python = "<3.9"
|
||||
version = ">=0.1.2"
|
||||
|
||||
[package.extras]
|
||||
docs = ["ryd"]
|
||||
jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"]
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml"
|
||||
marker = "platform_python_implementation == \"CPython\" and python_version < \"3.9\""
|
||||
name = "ruamel.yaml.clib"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.2.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||
name = "toml"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "0.10.0"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||
name = "typed-ast"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.4.1"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
name = "urllib3"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
version = "1.25.9"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotlipy (>=0.6.0)"]
|
||||
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"]
|
||||
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "5fe506efe8fd4c42bb9ef248033a7bd074d9f66e5b6a03727aa9cc554ae1d8fb"
|
||||
python-versions = "^3.7"
|
||||
|
||||
[metadata.files]
|
||||
appdirs = [
|
||||
{file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"},
|
||||
{file = "appdirs-1.4.3.tar.gz", hash = "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92"},
|
||||
]
|
||||
attrs = [
|
||||
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
|
||||
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
|
||||
]
|
||||
bitstring = [
|
||||
{file = "bitstring-3.1.6-py2-none-any.whl", hash = "sha256:e392819965e7e0246e3cf6a51d5a54e731890ae03ebbfa3cd0e4f74909072096"},
|
||||
{file = "bitstring-3.1.6-py3-none-any.whl", hash = "sha256:7b60b0c300d0d3d0a24ec84abfda4b0eaed3dc56dc90f6cbfe497166c9ad8443"},
|
||||
{file = "bitstring-3.1.6.tar.gz", hash = "sha256:c97a8e2a136e99b523b27da420736ae5cb68f83519d633794a6a11192f69f8bf"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"},
|
||||
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2020.4.5.1-py2.py3-none-any.whl", hash = "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304"},
|
||||
{file = "certifi-2020.4.5.1.tar.gz", hash = "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"},
|
||||
]
|
||||
chardet = [
|
||||
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
|
||||
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
|
||||
{file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
|
||||
{file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"},
|
||||
{file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"},
|
||||
]
|
||||
lxml = [
|
||||
{file = "lxml-4.5.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c"},
|
||||
{file = "lxml-4.5.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd"},
|
||||
{file = "lxml-4.5.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261"},
|
||||
{file = "lxml-4.5.0-cp27-cp27m-win32.whl", hash = "sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89"},
|
||||
{file = "lxml-4.5.0-cp27-cp27m-win_amd64.whl", hash = "sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a"},
|
||||
{file = "lxml-4.5.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128"},
|
||||
{file = "lxml-4.5.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca"},
|
||||
{file = "lxml-4.5.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb"},
|
||||
{file = "lxml-4.5.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8"},
|
||||
{file = "lxml-4.5.0-cp35-cp35m-win32.whl", hash = "sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77"},
|
||||
{file = "lxml-4.5.0-cp35-cp35m-win_amd64.whl", hash = "sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081"},
|
||||
{file = "lxml-4.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9"},
|
||||
{file = "lxml-4.5.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717"},
|
||||
{file = "lxml-4.5.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15"},
|
||||
{file = "lxml-4.5.0-cp36-cp36m-win32.whl", hash = "sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7"},
|
||||
{file = "lxml-4.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012"},
|
||||
{file = "lxml-4.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6"},
|
||||
{file = "lxml-4.5.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679"},
|
||||
{file = "lxml-4.5.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc"},
|
||||
{file = "lxml-4.5.0-cp37-cp37m-win32.whl", hash = "sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a"},
|
||||
{file = "lxml-4.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8"},
|
||||
{file = "lxml-4.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72"},
|
||||
{file = "lxml-4.5.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1"},
|
||||
{file = "lxml-4.5.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a"},
|
||||
{file = "lxml-4.5.0-cp38-cp38-win32.whl", hash = "sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f"},
|
||||
{file = "lxml-4.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3"},
|
||||
{file = "lxml-4.5.0.tar.gz", hash = "sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60"},
|
||||
]
|
||||
mysqlclient = [
|
||||
{file = "mysqlclient-1.4.6-cp36-cp36m-win_amd64.whl", hash = "sha256:4c82187dd6ab3607150fbb1fa5ef4643118f3da122b8ba31c3149ddd9cf0cb39"},
|
||||
{file = "mysqlclient-1.4.6-cp37-cp37m-win_amd64.whl", hash = "sha256:9e6080a7aee4cc6a06b58b59239f20f1d259c1d2fddf68ddeed242d2311c7087"},
|
||||
{file = "mysqlclient-1.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:f646f8d17d02be0872291f258cce3813497bc7888cd4712a577fd1e719b2f213"},
|
||||
{file = "mysqlclient-1.4.6.tar.gz", hash = "sha256:f3fdaa9a38752a3b214a6fe79d7cae3653731a53e577821f9187e67cbecb2e16"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"},
|
||||
{file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"},
|
||||
]
|
||||
peewee = [
|
||||
{file = "peewee-3.13.3.tar.gz", hash = "sha256:1269a9736865512bd4056298003aab190957afe07d2616cf22eaf56cb6398369"},
|
||||
]
|
||||
regex = [
|
||||
{file = "regex-2020.4.4-cp27-cp27m-win32.whl", hash = "sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f"},
|
||||
{file = "regex-2020.4.4-cp27-cp27m-win_amd64.whl", hash = "sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1"},
|
||||
{file = "regex-2020.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b"},
|
||||
{file = "regex-2020.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db"},
|
||||
{file = "regex-2020.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156"},
|
||||
{file = "regex-2020.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3"},
|
||||
{file = "regex-2020.4.4-cp36-cp36m-win32.whl", hash = "sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8"},
|
||||
{file = "regex-2020.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a"},
|
||||
{file = "regex-2020.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468"},
|
||||
{file = "regex-2020.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6"},
|
||||
{file = "regex-2020.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd"},
|
||||
{file = "regex-2020.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948"},
|
||||
{file = "regex-2020.4.4-cp37-cp37m-win32.whl", hash = "sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e"},
|
||||
{file = "regex-2020.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a"},
|
||||
{file = "regex-2020.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e"},
|
||||
{file = "regex-2020.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683"},
|
||||
{file = "regex-2020.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b"},
|
||||
{file = "regex-2020.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"},
|
||||
{file = "regex-2020.4.4-cp38-cp38-win32.whl", hash = "sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3"},
|
||||
{file = "regex-2020.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3"},
|
||||
{file = "regex-2020.4.4.tar.gz", hash = "sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142"},
|
||||
]
|
||||
requests = [
|
||||
{file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
|
||||
{file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
|
||||
]
|
||||
"ruamel.yaml" = [
|
||||
{file = "ruamel.yaml-0.16.10-py2.py3-none-any.whl", hash = "sha256:0962fd7999e064c4865f96fb1e23079075f4a2a14849bcdc5cdba53a24f9759b"},
|
||||
{file = "ruamel.yaml-0.16.10.tar.gz", hash = "sha256:099c644a778bf72ffa00524f78dd0b6476bca94a1da344130f4bf3381ce5b954"},
|
||||
]
|
||||
"ruamel.yaml.clib" = [
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c6d040d0396c28d3eaaa6cb20152cb3b2f15adf35a0304f4f40a3cf9f1d2448"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d55386129291b96483edcb93b381470f7cd69f97585829b048a3d758d31210a"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win32.whl", hash = "sha256:8073c8b92b06b572e4057b583c3d01674ceaf32167801fe545a087d7a1e8bf52"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27m-win_amd64.whl", hash = "sha256:615b0396a7fad02d1f9a0dcf9f01202bf9caefee6265198f252c865f4227fcc6"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a0ff786d2a7dbe55f9544b3f6ebbcc495d7e730df92a08434604f6f470b899c5"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:ea4362548ee0cbc266949d8a441238d9ad3600ca9910c3fe4e82ee3a50706973"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:77556a7aa190be9a2bd83b7ee075d3df5f3c5016d395613671487e79b082d784"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win32.whl", hash = "sha256:392b7c371312abf27fb549ec2d5e0092f7ef6e6c9f767bfb13e83cb903aca0fd"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp35-cp35m-win_amd64.whl", hash = "sha256:ed5b3698a2bb241b7f5cbbe277eaa7fe48b07a58784fba4f75224fd066d253ad"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:7aee724e1ff424757b5bd8f6c5bbdb033a570b2b4683b17ace4dbe61a99a657b"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d0d3ac228c9bbab08134b4004d748cf9f8743504875b3603b3afbb97e3472947"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win32.whl", hash = "sha256:f9dcc1ae73f36e8059589b601e8e4776b9976effd76c21ad6a855a74318efd6e"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e77424825caba5553bbade750cec2277ef130647d685c2b38f68bc03453bac6"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d10e9dd744cf85c219bf747c75194b624cc7a94f0c80ead624b06bfa9f61d3bc"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:550168c02d8de52ee58c3d8a8193d5a8a9491a5e7b2462d27ac5bf63717574c9"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win32.whl", hash = "sha256:57933a6986a3036257ad7bf283529e7c19c2810ff24c86f4a0cfeb49d2099919"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b1b7fcee6aedcdc7e62c3a73f238b3d080c7ba6650cd808bce8d7761ec484070"},
|
||||
{file = "ruamel.yaml.clib-0.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:be018933c2f4ee7de55e7bd7d0d801b3dfb09d21dad0cce8a97995fd3e44be30"},
|
||||
{file = "ruamel.yaml.clib-0.2.0.tar.gz", hash = "sha256:b66832ea8077d9b3f6e311c4a53d06273db5dc2db6e8a908550f3c14d67e718c"},
|
||||
]
|
||||
toml = [
|
||||
{file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
|
||||
{file = "toml-0.10.0-py2.py3-none-any.whl", hash = "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"},
|
||||
{file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"},
|
||||
]
|
||||
typed-ast = [
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
|
||||
{file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
|
||||
{file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
|
||||
{file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
|
||||
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
|
||||
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
|
||||
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"},
|
||||
{file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"},
|
||||
]
|
31
pyproject.toml
Normal file
31
pyproject.toml
Normal file
@ -0,0 +1,31 @@
|
||||
[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"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Adam Goldsmith <adam@adamgoldsmith.name>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
requests = "^2.23.0"
|
||||
"ruamel.yaml" = "^0.16.10"
|
||||
bitstring = "^3.1.6"
|
||||
lxml = "^4.5.0"
|
||||
peewee = "^3.13.2"
|
||||
mysqlclient = "^1.4.6"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^19.10b0"
|
||||
isort = "^4.3.21"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
34
sqlExport.py
34
sqlExport.py
@ -3,7 +3,8 @@
|
||||
from datetime import datetime
|
||||
|
||||
from common import membershipworks
|
||||
from lib.mw_models import database, Label, Member, MemberLabel, Transaction
|
||||
from lib.mw_models import Label, Member, MemberLabel, Transaction, database
|
||||
|
||||
|
||||
@database.atomic()
|
||||
def main():
|
||||
@ -11,32 +12,31 @@ def main():
|
||||
database.create_tables([Label, Member, MemberLabel, Transaction])
|
||||
|
||||
print("Updating labels")
|
||||
labels = membershipworks._parse_flags()['labels']
|
||||
Label \
|
||||
.insert_many([{'label_id': v, 'label': k} for k, v in labels.items()]) \
|
||||
.on_conflict(action="update", preserve=[Label.label]) \
|
||||
.execute()
|
||||
labels = membershipworks._parse_flags()["labels"]
|
||||
Label.insert_many(
|
||||
[{"label_id": v, "label": k} for k, v in labels.items()]
|
||||
).on_conflict(action="update", preserve=[Label.label]).execute()
|
||||
|
||||
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']]:
|
||||
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
|
||||
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 labels
|
||||
for label, label_id in membershipworks._parse_flags()['labels'].items():
|
||||
ml = MemberLabel(uid=member['Account ID'], label_id=label_id)
|
||||
for label, label_id in membershipworks._parse_flags()["labels"].items():
|
||||
ml = MemberLabel(uid=member["Account ID"], label_id=label_id)
|
||||
if member[label]:
|
||||
ml.magic_save()
|
||||
else:
|
||||
@ -49,14 +49,18 @@ def main():
|
||||
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])
|
||||
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
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
114
ucsAccounts.py
114
ucsAccounts.py
@ -8,82 +8,98 @@ from common import membershipworks
|
||||
|
||||
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: .*'])
|
||||
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]
|
||||
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])
|
||||
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()], [])
|
||||
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], [])
|
||||
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 = membershipworks.get_members(['Members', 'CMS Staff'],
|
||||
"lvl,phn,eml,lbl,nam,end,_id")
|
||||
members = membershipworks.get_members(
|
||||
["Members", "CMS 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))
|
||||
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
|
||||
"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)
|
||||
|
||||
"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
|
||||
|
||||
# "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
|
||||
|
||||
# "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"]
|
||||
"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))
|
||||
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))
|
||||
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