diff --git a/.gitignore b/.gitignore index aca0762..05b11af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # general things to ignore *.py[cod] +.pytest_cache/ + +conf.py # Mr Developer .idea/ diff --git a/HISTORY.rst b/HISTORY.rst new file mode 100644 index 0000000..45ef1e7 --- /dev/null +++ b/HISTORY.rst @@ -0,0 +1,16 @@ +.. :changelog: + +Release History +=============== + +1.0.0 (2018-07-02) +++++++++++++++++++ + +* Birth! + + +0.0.1 (2018-06-23) +++++++++++++++++++ + +* Frustration +* Conception diff --git a/README.md b/README.md index c063d0b..82cdf60 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,37 @@ sirena-quota ============ -It's the best way to control tickets quota in Sirena. \ No newline at end of file +It's the best way to control tickets quota in Sirena. +Notice, these scripts works with Sirena Web Services (SWC), not with Sirena XML Gate. + +Behold, the power of: +``` +$ python checker.py +OTA.UT - ok +$ python informer.py +``` + + +## Installation +First, initialize your virtual environment +``` +$ virtualenv .ve +$ source .ve/bin/activate +``` + +Install dependencies +``` +$ pip install -r requirements.txt +``` + +Setup configuration +``` +$ cp conf.sample.py conf.py +$ vim conf.py +✨🎩✨ +``` + +Finally, setup sqlite database +``` +$ python manage.py setup +``` diff --git a/checker.py b/checker.py new file mode 100644 index 0000000..65219c8 --- /dev/null +++ b/checker.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +import re +import requests +import logging +from utils import db_execute +import conf + + +def extract_ticket_quota(ticket_quota_response): + matches = re.findall(r'(\d+)<\/quota>', ticket_quota_response) + return int(matches[0]) + + +def get_ticket_quota(user, password, gateway='https://ws.sirena-travel.ru/swc-main/bookingService'): + """Returns ticket quota for PoS (ППр). + + This method calls `getTicketQuota` method of Sirena WS. + + :param user: Sirena WS user with supervisor permission in the PoS. + :param password: user password. + :param gateway: (optional) Sirena WS gateway to call getTicketQuota method. + :return: ticket quota. + :rtype: int + """ + rq = ''' + + + + 0 + + + ''' + r = requests.post(gateway, auth=(user, password), data=rq) + r.raise_for_status() + return extract_ticket_quota(r.text) + + +def save_check(db_name, account_code, ticket_quota): + db_execute( + db_name, + '''INSERT INTO quota_check (account, quota) + VALUES ('{account}', {quota}) + '''.format(account=account_code, quota=ticket_quota) + ) + + +if __name__ == '__main__': + for code, account in conf.accounts.items(): + try: + save_check( + conf.db_name, + code, + get_ticket_quota(account['user'], account['password']) + ) + print('{:20} - ok'.format(code)) + except Exception as e: + print('{:20} - error'.format(code)) + logging.critical(e, exc_info=True) diff --git a/conf.sample.py b/conf.sample.py new file mode 100644 index 0000000..6b30ba6 --- /dev/null +++ b/conf.sample.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +accounts = { + 'OTA.TCH': { + 'user': 'ota_grs101', + 'password': 'secret', + }, + 'OTA.UT': { + 'user': 'ota_grs102', + 'password': 'newsecret', + }, +} +sender = ('Anton Yakovlev', 'anton.yakovlev@a.gentlemantravel.club') +recipient_info = 'airquota@ota.ru' +recipient_alert = 'airquota.alert@ota.ru' +db_name = 'db/quota.db' +smtp_gateway= 'email-smtp.eu-west-1.amazonaws.com' +smtp_port = 587 +smtp_user = 'aws_ses_user' +smtp_password = 'aws_ses_user_password' diff --git a/db/.gitignore b/db/.gitignore new file mode 100644 index 0000000..5e7d273 --- /dev/null +++ b/db/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/informer.py b/informer.py new file mode 100644 index 0000000..1badcf4 --- /dev/null +++ b/informer.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +import smtplib +import email.utils +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +import time +from utils import db_execute +import conf + + +def prepare_body(accounts_info, sender_name): + """Returns message body in text and html format. + + Structure of accounts_info + >>> accounts_info = [{ + >>> 'code': 'OTA.TCH', + >>> 'quota': 985, + >>> 'datetime': '2018-06-23 19:26:16 (MSK)', + >>> 'alert': False, + >>> }] + + :param accounts_info: list, accounts info, see above description of element structure. + :param sender_name: str, name of sender used in signature. + :return: tuple of two strings, (body_text, body_html) + :rtype: tuple + """ + status_msg = '' + alert_accounts = list() + for account in accounts_info: + if account['alert']: + alert_accounts.append(account['code']) + status_msg += '{account} - {quota}, проверено {dt}\r\n'\ + .format(account=account['code'], quota=account['quota'], dt=account['datetime']) + alert_msg = '' + if len(alert_accounts): + alert_msg = 'Срочно запросите квоту для: ' + ', '.join(alert_accounts) + '!\r\n\r\n' + body_text = ( + 'Приветствую\r\n\r\n' + '{alert}' + 'Состояние стоков:\r\n{status_msg}' + '\r\n\r\n' + '--\r\n' + 'Дружески,\r\n' + '{sender}' + ) + body_html = ( + '' + '' + '' + '

Приветствую

' + '{alert}' + '

Состояние стоков:
{status_msg}

' + '

' + '

' + '--
' + 'Дружески,
' + '{sender}' + '

' + '' + '' + ) + return ( + body_text.format(alert=alert_msg, status_msg=status_msg, sender=sender_name), + body_html.format(alert=alert_msg, status_msg=status_msg.replace('\r\n', '
'), sender=sender_name) + ) + + +def send_mail(sender, recipient, subject, body, smtp_user, smtp_password, + smtp_gateway='email-smtp.eu-west-1.amazonaws.com', smtp_port=587): + """Sends mail via SMTP server. + + :param sender: tuple of two strings, ('Sender Name', 'sender@email.com') + :param recipient: str, recipient email, e.g. 'recipient@email.com' + :param subject: str, email subject + :param body: tuple of two strings, (body_text, body_html) + :param smtp_gateway: str, smtp server endpoint + :param smtp_port: int, smtp server port + :param smtp_user: str, login of smtp user + :param smtp_password: str, password of smtp user + """ + body_text, body_html = body + msg = MIMEMultipart('alternative') + msg['Subject'] = subject + msg['From'] = email.utils.formataddr(sender) + msg['To'] = recipient + msg.attach(MIMEText(body_text, 'plain')) + msg.attach(MIMEText(body_html, 'html')) + server = smtplib.SMTP(smtp_gateway, smtp_port) + server.ehlo() + server.starttls() + server.ehlo() + server.login(smtp_user, smtp_password) + server.sendmail(sender[1], recipient, msg.as_string()) + server.close() + + +if __name__ == '__main__': + rows = db_execute( + conf.db_name, + 'SELECT account, quota, created_at FROM quota_check GROUP BY account HAVING MAX(created_at)' + ) + info_items = list() + alert = False + for row in rows: + if row[0] not in conf.accounts: + continue # unknown account code + info_items.append({ + 'code': row[0], + 'quota': row[1], + 'datetime': time.strftime('%Y-%m-%d %H:%M:%S (%Z)', time.localtime(row[2])), + 'alert': row[1] <= conf.accounts[row[0]].get('alert', 0), + }) + alert = info_items[-1]['alert'] or alert + send_mail(conf.sender, conf.recipient_info, 'Состояние стоков в Сирене', + prepare_body(info_items, conf.sender[0]), conf.smtp_user, conf.smtp_password) + if alert: + send_mail(conf.sender, conf.recipient_alert, 'Срочно пополните сток в Сирене', + prepare_body(info_items, conf.sender[0]), conf.smtp_user, conf.smtp_password) diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..64db17c --- /dev/null +++ b/manage.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +import argparse +from utils import db_execute +from conf import db_name + + +def setup_store(db_name): + db_execute( + db_name, + '''CREATE TABLE IF NOT EXISTS quota_check ( + id INTEGER PRIMARY KEY, + account VARCHAR, + quota INTEGER, + created_at integer(4) not null default (strftime('%s','now')) + )''' + ) + + +def drop_store(db_name): + db_execute(db_name, 'DROP TABLE IF EXISTS quota_check') + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('command', type=str, choices=['setup', 'drop'], + help='what to do with store of quota checks') + return parser.parse_args() + + +if __name__ == '__main__': + args = parse_args() + if args.command == 'setup': + setup_store(db_name) + elif args.command == 'drop': + drop_store(db_name) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c121064 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests==2.19.1 +pytest==3.6.2 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..b92b1c5 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from .test_checker import TestChecker diff --git a/tests/test_checker.py b/tests/test_checker.py new file mode 100644 index 0000000..fe79a15 --- /dev/null +++ b/tests/test_checker.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +import pytest +from checker import extract_ticket_quota + + +class TestChecker: + @pytest.mark.parametrize('rs', [ + '958', + '0958' + ]) + def test_extract_quota(self, rs): + assert extract_ticket_quota(rs) == 958 diff --git a/tests/test_db.py b/tests/test_db.py new file mode 100644 index 0000000..2e6ae9c --- /dev/null +++ b/tests/test_db.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +import sqlite3 +import os +import pytest +from manage import setup_store + + +class TestDB(): + db_name = 'db/test.db' + + def test_init_db(self): + try: + os.remove(self.db_name) + except OSError: + pytest.fail('Unexpected error trying to delete {}'.format(self.db_name), pytrace=True) + conn = sqlite3.connect(self.db_name) + conn.close() + assert os.path.isfile(self.db_name) + + def test_setup_store(self): + try: + setup_store(self.db_name) + except sqlite3.Error: + pytest.fail('Unexpected error trying to setup store', pytrace=True) diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..354148c --- /dev/null +++ b/utils.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +import sqlite3 + + +def db_execute(db_name, query): + """Executes SQL query in sqlite database.""" + conn = sqlite3.connect(db_name) + cursor = conn.cursor() + cursor.execute(query) + rows = cursor.fetchall() + conn.commit() + conn.close() + return rows