updated changes
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2019-09-27 08:45:12 +02:00
parent 999beb6269
commit 92cf128433
14 changed files with 532 additions and 135 deletions

139
.dockerignore Normal file
View File

@@ -0,0 +1,139 @@
infomentor.db
# Created by https://www.gitignore.io/api/python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
### Python Patch ###
.venv/
### Python.VirtualEnv Stack ###
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
pip-selfcheck.json
# End of https://www.gitignore.io/api/python
infomentor.ini
infomentor.db
log.*

16
.drone.yml Normal file
View File

@@ -0,0 +1,16 @@
kind: pipeline
type: docker
name: default
steps:
- name: deploy
image: plugins/docker
settings:
registry: registry.d1v3.de
repo: registry.d1v3.de/infomentor
username:
from_secret: docker_username
password:
from_secret: docker_password
tags: ["commit_${DRONE_COMMIT}","build_${DRONE_BUILD_NUMBER}", "latest"]

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM python:3.7.3-stretch
COPY requirements.txt /tmp/
RUN pip install -r /tmp/requirements.txt
COPY . /tmp/infomentor
RUN pip install /tmp/infomentor
RUN useradd --create-home appuser
WORKDIR /home/appuser
USER appuser
VOLUME ["/home/appuser"]
CMD [ "python", "-m", "infomentor" ]

View File

@@ -8,7 +8,7 @@ This tool is designed to check the infomentor portal and send notifications usin
python3 -m venv venv
source venv/bin/activate
python setup.py install
python -m infomentor
infomentor
```
After the first run a `infomentor.ini` file is available which has a few values to be entered.
@@ -21,19 +21,19 @@ After the first run a `infomentor.ini` file is available which has a few values
Provide the username and password for infomentor.
```
source venv/bin/activate
python -m infomentor --adduser <username>
adduser --username <username>
```
### Step 2 add notification mechanism
```
source venv/bin/activate
python -m infomentor --addmail <username>
addmail --username <username>
```
or
```
source venv/bin/activate
python -m infomentor --addpushover <username>
addpushover --username <username>
```
### Step 3 (optional) Add iCloud calendar
@@ -42,7 +42,7 @@ It is capable of syncing all the infomentor calendar elements to icloud calendar
```
source venv/bin/activate
python -m infomentor --addcalendar <username>
addcalendar --username <username>
```
## NB
@@ -51,3 +51,21 @@ The login process is a bit scary and mostly hacked. It happens often on the firs
The script shall be run every 10 minutes, that will keep the session alive and minimize errors.
## Docker
This could be run within docker. You it has a volume `/home/appuser` where all the data is stored. In favour of accessing it from a webserver you should bindmount it.
There also the infomentor.ini should be placed.
Build the container by `docker build -t infomentor:latest .` and run it like this:
```
docker run -v '/var/docker/infomentor/:/home/appuser' infomentor:latest
```
for adding an user or all the commands run it adding -it to it, like:
```
docker run -it -v '/var/docker/infomentor/:/home/appuser' infomentor:latest adduser
```

View File

@@ -12,3 +12,5 @@ server = example.org
username = infomentor@example.org
password = secret1234
[healthcheck]
url = https://health.d1v3.de/ping/123123123123123

View File

@@ -0,0 +1 @@
#from infomentor.__main__ import *

View File

@@ -4,16 +4,17 @@ import argparse
import datetime
import sys
import os
from infomentor import db, model, connector, informer
import requests
from infomentor import db, model, connector, informer, config
logformat = "{asctime} - {name:25s} - {levelname:8s} - {message}"
logformat = "{asctime} - {name:25s}[{filename:20s}:{lineno:3d}] - {levelname:8s} - {message}"
def logtofile():
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler("log.txt", maxBytes=51200, backupCount=5)
handler = RotatingFileHandler("log.txt", maxBytes=1024*1024, backupCount=10)
logging.basicConfig(
level=logging.INFO, format=logformat, handlers=[handler], style="{"
)
@@ -28,12 +29,14 @@ def parse_args(arglist):
parser.add_argument(
"--nolog", action="store_true", help="print log instead of logging to file"
)
parser.add_argument("--adduser", type=str, help="add user")
parser.add_argument("--addfake", type=str, help="add fake")
parser.add_argument("--addpushover", type=str, help="add pushover")
parser.add_argument("--addmail", type=str, help="add mail")
parser.add_argument("--addcalendar", type=str, help="add icloud calendar")
parser.add_argument("--adduser", action='store_true', help="add user")
parser.add_argument("--addfake", action='store_true', help="add fake")
parser.add_argument("--addpushover", action='store_true', help="add pushover")
parser.add_argument("--addmail", action='store_true', help="add mail")
parser.add_argument("--addcalendar", action='store_true', help="add icloud calendar")
parser.add_argument("--addinvitation", action='store_true', help="add calendar invitation")
parser.add_argument("--test", action="store_true", help="test")
parser.add_argument("--username", type=str, nargs='?', help="username")
args = parser.parse_args(arglist)
return args
@@ -104,6 +107,18 @@ def add_calendar(username):
)
session.commit()
def add_invitation(username):
session = db.get_db()
user = session.query(model.User).filter(model.User.name == username).one_or_none()
if user is None:
print("user does not exist")
return
else:
print(f"Adding Mail for calendar invitation for user: {username}")
mail = input("Mail: ")
user.invitation = model.Invitation(email=mail)
session.commit()
def add_mail(username):
session = db.get_db()
@@ -123,6 +138,10 @@ def add_mail(username):
def notify_users():
logger = logging.getLogger(__name__)
session = db.get_db()
cfg = config.load()
if cfg["healthchecks"]["url"] != "":
requests.get(cfg["healthchecks"]["url"])
for user in session.query(model.User):
logger.info("==== USER: %s =====", user.name)
if user.password == "":
@@ -187,15 +206,17 @@ def main():
logger.info("EXITING - PREVIOUS IS RUNNING")
raise Exception()
if args.addfake:
add_fake(args.addfake)
add_fake(args.username)
elif args.adduser:
add_user(args.adduser)
add_user(args.username)
elif args.addpushover:
add_pushover(args.addpushover)
add_pushover(args.username)
elif args.addmail:
add_mail(args.addmail)
add_mail(args.username)
elif args.addcalendar:
add_calendar(args.addcalendar)
add_calendar(args.username)
elif args.addinvitation:
add_invitation(args.username)
else:
notify_users()
except Exception as e:
@@ -204,6 +225,64 @@ def main():
finally:
logger.info("EXITING--------------------- %s", os.getpid())
def run_notify():
run_without_args(notify_users)
def run_adduser():
run_with_args(add_user)
def run_addfake():
run_with_args(add_fake)
def run_addpushover():
run_with_args(add_pushover)
def run_addmail():
run_with_args(add_mail)
def run_addcalendar():
run_with_args(add_calendar)
def run_addinvitation():
run_with_args(add_invitation)
def run_with_args(fct):
args = parse_args(sys.argv[1:])
logtofile()
logger = logging.getLogger("Infomentor Notifier")
logger.info("STARTING-------------------- %s", os.getpid())
lock = flock.flock()
try:
if not lock.aquire():
logger.info("EXITING - PREVIOUS IS RUNNING")
raise Exception()
if args.username is None:
print('Provide Username using --username')
raise Exception('No username provided')
fct(args.username)
except Exception as e:
logger.info("Exceptional exit")
logger.exception("Info")
finally:
logger.info("EXITING--------------------- %s", os.getpid())
def run_without_args(fct):
args = parse_args(sys.argv[1:])
logtofile()
logger = logging.getLogger("Infomentor Notifier")
logger.info("STARTING-------------------- %s", os.getpid())
try:
lock = flock.flock()
if not lock.aquire():
logger.info("EXITING - PREVIOUS IS RUNNING")
raise Exception()
fct()
except Exception as e:
logger.info("Exceptional exit")
logger.exception("Info")
finally:
logger.info("EXITING--------------------- %s", os.getpid())
if __name__ == "__main__":
main()

View File

@@ -3,27 +3,36 @@ import os
_config = None
_defaults = {
"pushover": {
"apikey": "",
},
"general": {
"secretkey": "",
"baseurl": "",
"adminmail": "",
"im1url": "https://im1.infomentor.de/Germany/Germany/Production",
"mimrul": "https://mein.infomentor.de",
},
"smtp": {
"server": "",
"username": "",
"password": "",
},
"healthchecks": {
"url": "",
},
}
def _set_defaults(config):
config.add_section("pushover")
config.add_section("general")
config.add_section("smtp")
config["pushover"]["apikey"] = ""
config["general"]["secretkey"] = ""
config["general"]["baseurl"] = ""
config["general"]["adminmail"] = ""
config["general"]["im1url"] = "https://im1.infomentor.de/Germany/Germany/Production"
config["general"]["mimurl"] = "https://mein.infomentor.de"
config["smtp"]["server"] = ""
config["smtp"]["username"] = ""
config["smtp"]["password"] = ""
config = _defaults
def load(cfg_file="infomentor.ini"):
"""Load the config from the file"""
global _config
if _config is None:
_config = configparser.ConfigParser()
_config = configparser.ConfigParser(_defaults)
if not os.path.isfile(cfg_file):
_set_defaults(_config)
save(cfg_file)

View File

@@ -10,17 +10,19 @@ import contextlib
import logging
import urllib.parse
import uuid
import glob
import hashlib
from infomentor import model, config
class InfomentorFile(object):
"""Represent a file which is downloaded"""
def __init__(self, directory, filename):
def __init__(self, directory, filename, seed=''):
if directory is None:
raise Exception("directory is required")
self.filename = filename
self.randomid = str(uuid.uuid4())
self.randomid = hashlib.sha1('{}{}'.format(filename, seed).encode('utf-8')).hexdigest()
self.directory = directory
@property
@@ -232,7 +234,7 @@ class Infomentor(object):
def _download_file(self, url, directory, filename=None):
"""download a file with provided filename"""
file = InfomentorFile(directory, filename)
file = InfomentorFile(directory, filename, seed=url)
self.logger.info("to (randomized) directory %s", file.targetdir)
url = self._mim_url(url)
self._do_get(url)
@@ -291,7 +293,7 @@ class Infomentor(object):
self.logger.exception("failed to store attachment")
news = model.News(**storenewsdata)
with contextlib.suppress(Exception):
news.imagefile = self.get_newsimage(id)
news.imagefile = self.get_newsimage(article_json["id"])
return news
def get_article(self, id):

View File

@@ -2,11 +2,15 @@ from datetime import datetime
import sys
from bs4 import BeautifulSoup
import time
import caldav
from caldav.elements import dav, cdav
from lxml import etree
import requests
from requests.auth import HTTPBasicAuth
import logging
_logger = logging.getLogger(__name__)
class iCloudConnector(object):
@@ -14,15 +18,9 @@ class iCloudConnector(object):
icloud_url = "https://caldav.icloud.com"
username = None
password = None
propfind_principal = (
u"""<?xml version="1.0" encoding="utf-8"?><propfind xmlns='DAV:'>"""
u"""<prop><current-user-principal/></prop></propfind>"""
)
propfind_calendar_home_set = (
u"""<?xml version="1.0" encoding="utf-8"?><propfind xmlns='DAV:' """
u"""xmlns:cd='urn:ietf:params:xml:ns:caldav'><prop>"""
u"""<cd:calendar-home-set/></prop></propfind>"""
)
propfind_principal = '<A:propfind xmlns:A="DAV:"><A:prop><A:current-user-principal/></A:prop></A:propfind>'
propfind_calendar_home_set = "<propfind xmlns='DAV:' xmlns:cd='urn:ietf:params:xml:ns:caldav'><prop> <cd:calendar-home-set/></prop></propfind>"
def __init__(self, username, password, **kwargs):
self.username = username
@@ -32,6 +30,7 @@ class iCloudConnector(object):
self.discover()
self.get_calendars()
# discover: connect to icloud using the provided credentials and discover
#
# 1. The principal URL
@@ -45,40 +44,53 @@ class iCloudConnector(object):
# given credentials
headers = {"Depth": "1"}
auth = HTTPBasicAuth(self.username, self.password)
principal_response = requests.request(
principal_response = self.repeated_request(
"PROPFIND",
self.icloud_url,
auth=auth,
headers=headers,
data=self.propfind_principal.encode("utf-8"),
data=self.propfind_principal
)
if principal_response.status_code != 207:
print("Failed to retrieve Principal: ", principal_response.status_code)
exit(-1)
# Parse the resulting XML response
soup = BeautifulSoup(principal_response.content, "lxml")
self.principal_path = (
soup.find("current-user-principal").find("href").get_text()
)
discovery_url = self.icloud_url + self.principal_path
_logger.debug("Discovery url {}".format(discovery_url))
# Next use the discovery URL to get more detailed properties - such as
# the calendar-home-set
home_set_response = requests.request(
home_set_response = self.repeated_request(
"PROPFIND",
discovery_url,
auth=auth,
headers=headers,
data=self.propfind_calendar_home_set.encode("utf-8"),
data=self.propfind_calendar_home_set,
)
_logger.debug("Result code: {}".format(home_set_response.status_code))
if home_set_response.status_code != 207:
print("Failed to retrieve calendar-home-set", home_set_response.status_code)
exit(-1)
_logger.error("Failed to retrieve calendar-home-set {}".format(home_set_response.status_code))
raise Exception("failed to retrieve calender home set {}".format(home_set_response.content))
# And then extract the calendar-home-set URL
soup = BeautifulSoup(home_set_response.content, "lxml")
self.calendar_home_set_url = soup.find(
"href", attrs={"xmlns": "DAV:"}
).get_text()
def repeated_request(self, *args, **kwargs):
for _ in range(0, 5):
response = requests.request(
*args, **kwargs
)
_logger.debug("Request result code: {}".format(response.status_code))
if response.status_code != 207:
_logger.error("Failed to retrieve response: {}".format(response.status_code))
_logger.error("Retry")
time.sleep(0.25)
if response.status_code == 207:
break
else:
raise Exception("failed to retrieve {} {}".format(response.content, response.headers))
return response
# get_calendars
# Having discovered the calendar-home-set url
# we can create a local object to control calendars (thin wrapper around
@@ -123,3 +135,4 @@ class iCloudConnector(object):
):
# to do
pass

View File

@@ -30,6 +30,7 @@ class Informer(object):
self.logger = logger or logging.getLogger(__name__)
self.user = user
self.im = im
self.cal = None
def send_status_update(self, text):
"""In case something unexpected happends and the user has activated the feature to get notified about it, this will send out the information"""
@@ -56,11 +57,12 @@ class Informer(object):
news = (
session.query(model.News)
.filter(model.News.news_id == news_entry['id'])
.filter(model.News.date == news_entry['publishedDate'])
.with_parent(self.user, "news")
.one_or_none()
)
if news is not None:
self.logger.debug('Skipping news')
self.logger.debug('Skipping news %s', news_entry['id'])
continue
news = self.im.get_news_article(news_entry)
self._notify_news(news)
@@ -68,6 +70,9 @@ class Informer(object):
session.commit()
def _notify_news(self, news):
if self.user.notification is None:
self.logger.debug('Warn: no notification for user')
return
if self.user.notification.ntype == model.Notification.Types.PUSHOVER:
self._notify_news_pushover(news)
elif self.user.notification.ntype == model.Notification.Types.EMAIL:
@@ -233,77 +238,128 @@ class Informer(object):
s.send_message(mail)
s.quit()
def _send_invitation(self, calobj, to, fr="infomentor@09a.de"):
event = calobj.subcomponents[0]
eml_body = event['description']
eml_body_bin = event['description']
msg = MIMEMultipart('mixed')
msg['Reply-To']=fr
msg['Subject'] = event['summary']
msg['From'] = fr
msg['To'] = to
part_email = MIMEText(eml_body,"html")
part_cal = MIMEText(calobj.to_ical().decode('utf-8'),'calendar;method=REQUEST')
msgAlternative = MIMEMultipart('alternative')
msg.attach(msgAlternative)
ical_atch = MIMEBase('application/ics',' ;name="%s"'%("invite.ics"))
ical_atch.set_payload(calobj.to_ical().decode('utf-8'))
encoders.encode_base64(ical_atch)
ical_atch.add_header('Content-Disposition', 'attachment; filename="%s"'%("invite.ics"))
eml_atch = MIMEBase('text/plain','')
eml_atch.set_payload('')
encoders.encode_base64(eml_atch)
eml_atch.add_header('Content-Transfer-Encoding', "")
msgAlternative.attach(part_email)
msgAlternative.attach(part_cal)
self._send_mail(msg)
def _setup_icloudconnector(self):
if self.cal is None:
try:
icx = icloudcalendar.iCloudConnector(
self.user.icalendar.icloud_user, self.user.icalendar.password
)
cname = self.user.icalendar.calendarname
self.cal = icx.get_named_calendar(cname)
if not self.cal:
self.cal = icx.create_calendar(cname)
self.logger.warn("using icloud")
except Exception as e:
self.logger.exception("using icloud dummy connector")
class Dummy(object):
def add_event(self, *args, **kwargs):
pass
self.cal = Dummy()
def _write_icalendar(self, calend):
try:
self._setup_icloudconnector()
self.cal.add_event(calend.to_ical())
except Exception as e:
self.logger.exception('Calendar failed')
def update_calendar(self):
session = db.get_db()
if self.user.icalendar is None:
if self.user.icalendar is None and self.user.invitation is None:
return
icx = icloudcalendar.iCloudConnector(
self.user.icalendar.icloud_user, self.user.icalendar.password
)
cname = self.user.icalendar.calendarname
cal = icx.get_named_calendar(cname)
if not cal:
cal = icx.create_calendar(cname)
calentries = self.im.get_calendar()
for entry in calentries:
self.logger.debug(entry)
uid = "infomentor_{}".format(entry["id"])
event_details = self.im.get_event(entry["id"])
self.logger.debug(event_details)
calend = Calendar()
event = Event()
event.add("uid", "infomentor_{}".format(entry["id"]))
event.add("summary", entry["title"])
if not event_details["allDayEvent"]:
event.add("dtstart", dateparser.parse(entry["start"]))
event.add("dtend", dateparser.parse(entry["end"]))
else:
event.add("dtstart", dateparser.parse(entry["start"]).date())
event.add("dtend", dateparser.parse(entry["end"]).date())
description = event_details["notes"]
self.logger.debug(event_details['info'])
self.logger.debug(type(event_details['info']))
eventinfo = event_details['info']
self.logger.debug(eventinfo)
self.logger.debug(type(eventinfo))
for res in eventinfo['resources']:
f = self.im.download_file(res["url"], directory="files")
description += """\nAttachment {0}: {2}/{1}""".format(
res['title'], urllib.parse.quote(f), cfg["general"]["baseurl"]
)
event.add("description", description)
calend.add_component(event)
new_cal_entry = calend.to_ical().replace(b"\r", b"")
new_cal_hash = hashlib.sha1(new_cal_entry).hexdigest()
session = db.get_db()
storedata = {
"calendar_id": uid,
"ical": new_cal_entry,
"hash": new_cal_hash,
}
calendarentry = (
session.query(model.CalendarEntry)
.filter(model.CalendarEntry.calendar_id == uid)
.with_parent(self.user, "calendarentries")
.one_or_none()
)
if calendarentry is not None:
if calendarentry.hash == new_cal_hash:
self.logger.info("no change for calendar entry {}".format(uid))
continue
try:
calentries = self.im.get_calendar()
for entry in calentries:
self.logger.debug(entry)
uid = str(uuid.uuid5(uuid.NAMESPACE_URL, "infomentor_{}".format(entry["id"])))
event_details = self.im.get_event(entry["id"])
calend = Calendar()
event = Event()
event.add("uid", uid)
event.add("summary", entry["title"])
event.add("dtstamp", datetime.datetime.now())
if not event_details["allDayEvent"]:
event.add("dtstart", dateparser.parse(entry["start"]))
event.add("dtend", dateparser.parse(entry["end"]))
else:
self.logger.info("update calendar entry {}".format(uid))
for key, value in storedata.items():
setattr(calendarentry, key, value)
event.add("dtstart", dateparser.parse(entry["start"]).date())
event.add("dtend", dateparser.parse(entry["end"]).date())
else:
self.logger.info("new calendar entry {}".format(uid))
calendarentry = model.CalendarEntry(**storedata)
description = event_details["notes"]
eventinfo = event_details['info']
new_cal_entry = calend.to_ical().replace(b"\r", b"")
new_cal_hash = hashlib.sha1(new_cal_entry).hexdigest()
for res in eventinfo['resources']:
f = self.im.download_file(res["url"], directory="files")
description += """\nAttachment {0}: {2}/{1}""".format(
res['title'], urllib.parse.quote(f), cfg["general"]["baseurl"]
)
event.add("description", description)
self.user.calendarentries.append(calendarentry)
session.commit()
self.logger.debug(new_cal_entry.decode("utf-8"))
cal.add_event(calend.to_ical())
calend.add_component(event)
new_cal_entry = calend.to_ical().replace(b"\r", b"")
session = db.get_db()
storedata = {
"calendar_id": uid,
"ical": new_cal_entry,
"hash": new_cal_hash,
}
calendarentry = (
session.query(model.CalendarEntry)
.filter(model.CalendarEntry.calendar_id == uid)
.with_parent(self.user, "calendarentries")
.one_or_none()
)
if calendarentry is not None:
if calendarentry.hash == new_cal_hash:
self.logger.info("calendar entry UNCHANGED {}".format(uid))
continue
else:
self.logger.info("calendar entry UPDATED {}".format(uid))
for key, value in storedata.items():
setattr(calendarentry, key, value)
else:
self.logger.info("calendar entry NEW {}".format(uid))
calendarentry = model.CalendarEntry(**storedata)
self.logger.debug(new_cal_entry.decode("utf-8"))
if self.user.icalendar is not None:
self._write_icalendar(calend)
if self.user.invitation is not None:
self._send_invitation(calend, self.user.invitation.email)
self.user.calendarentries.append(calendarentry)
session.commit()
except Exception as e:
self.logger.exception('Calendar failed')

View File

@@ -35,6 +35,7 @@ class User(ModelBase):
notification = relationship("Notification", back_populates="user", uselist=False)
apistatus = relationship("ApiStatus", back_populates="user", uselist=False)
icalendar = relationship("ICloudCalendar", back_populates="user", uselist=False)
invitation = relationship("Invitation", back_populates="user", uselist=False)
wantstatus = Column(Boolean)
homeworks = relationship("Homework", back_populates="user")
news = relationship("News", back_populates="user")
@@ -236,3 +237,23 @@ class ICloudCalendar(ModelBase):
self.icloud_user,
self.calendarname,
)
class Invitation(ModelBase):
"""An icloud account with a calendar name"""
__tablename__ = "invitation_mails"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"))
email = Column(String)
user = relationship("User", back_populates="invitation", uselist=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __repr__(self):
return "<Invitation(email='%s')>" % (
self.email,
)

View File

@@ -1,10 +1,29 @@
requests
sqlalchemy
dateparser
python-pushover
pycrypto
bs4
flask
flask-bootstrap
caldav
icalendar
Click==7.0
Flask==1.1.1
Flask-Bootstrap==3.3.7.1
Jinja2==2.10.1
SQLAlchemy==1.3.6
Werkzeug==0.15.5
beautifulsoup4==4.8.0
bs4==0.0.1
caldav==0.6.1
certifi==2019.6.16
chardet==3.0.4
dateparser==0.7.1
dominate==2.3.5
icalendar==4.0.3
idna==2.8
itsdangerous==1.1.0
lxml==4.3.4
pycrypto==2.6.1
python-dateutil==2.8.0
python-pushover==0.4
pytz==2019.1
regex==2019.06.08
requests==2.22.0
six==1.12.0
tzlocal==1.5.1
urllib3==1.25.3
visitor==0.1.3
vobject==0.9.6.1

View File

@@ -8,6 +8,14 @@ setup(
author_email="matthias@bilger.info",
description="grab infomentor news and push or mail them",
packages=find_packages(),
entry_points = {
'console_scripts': [
'infomentor=infomentor:main',
'adduser=infomentor:run_adduser',
'addmail=infomentor:run_addmail',
'addpushover=infomentor:run_addpushover',
],
},
install_requires=[
"pycrypto",
"request",