From ee32dc2ebb667b8a57d9076f26e8e8a5bb90a853 Mon Sep 17 00:00:00 2001 From: Sergey Popinevskiy Date: Sat, 23 Jun 2018 11:47:57 +0300 Subject: [PATCH 1/7] Frustration and conception --- HISTORY.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 HISTORY.rst diff --git a/HISTORY.rst b/HISTORY.rst new file mode 100644 index 0000000..1a7a978 --- /dev/null +++ b/HISTORY.rst @@ -0,0 +1,11 @@ +.. :changelog: + +Release History +=============== + + +0.0.1 (2018-06-23) +++++++++++++++++++ + +* Frustration +* Conception From 859281a4b2bfaf8c0cd814c17ffe73fa318139cb Mon Sep 17 00:00:00 2001 From: Sergey Popinevskiy Date: Sat, 23 Jun 2018 15:53:34 +0300 Subject: [PATCH 2/7] Quota checker (#1) Implemented checker --- .gitignore | 3 +++ checker.py | 58 +++++++++++++++++++++++++++++++++++++++++++ conf.sample.py | 14 +++++++++++ db/.gitignore | 4 +++ manage.py | 35 ++++++++++++++++++++++++++ requirements.txt | 2 ++ tests/__init__.py | 2 ++ tests/test_checker.py | 12 +++++++++ tests/test_db.py | 24 ++++++++++++++++++ utils.py | 13 ++++++++++ 10 files changed, 167 insertions(+) create mode 100644 checker.py create mode 100644 conf.sample.py create mode 100644 db/.gitignore create mode 100644 manage.py create mode 100644 requirements.txt create mode 100644 tests/__init__.py create mode 100644 tests/test_checker.py create mode 100644 tests/test_db.py create mode 100644 utils.py 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/checker.py b/checker.py new file mode 100644 index 0000000..ecf914f --- /dev/null +++ b/checker.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +import re +import requests +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, ticket_quota): + db_execute( + db_name, + '''INSERT INTO quota_check (account, quota) + VALUES ('{account}', {quota}) + '''.format(account=account, quota=ticket_quota) + ) + + +if __name__ == '__main__': + for account in conf.accounts: + try: + save_check( + conf.db_name, + account['account'], + get_ticket_quota(account['user'], account['password']) + ) + print('{:20} - ok'.format(account['account'])) + except Exception: + print('{:20} - error'.format(account['account'])) diff --git a/conf.sample.py b/conf.sample.py new file mode 100644 index 0000000..5c1ff6f --- /dev/null +++ b/conf.sample.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +db_name = 'db/quota.db' +accounts = [ + { + 'user': 'ota_grs101', + 'password': 'secret', + 'account': 'OTA.TCH', + }, + { + 'user': 'ota_grs102', + 'password': 'newsecret', + 'account': 'OTA.UT', + } +] 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/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 From 86d36ec34fb3751b08db43961be05939e63e3a20 Mon Sep 17 00:00:00 2001 From: Sergey Popinevskiy Date: Mon, 25 Jun 2018 08:35:25 +0300 Subject: [PATCH 3/7] Informer (#2) Implemented informer --- checker.py | 12 ++--- conf.sample.py | 21 +++++---- informer.py | 118 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 informer.py diff --git a/checker.py b/checker.py index ecf914f..a6d0bd2 100644 --- a/checker.py +++ b/checker.py @@ -36,23 +36,23 @@ def get_ticket_quota(user, password, gateway='https://ws.sirena-travel.ru/swc-ma return extract_ticket_quota(r.text) -def save_check(db_name, account, ticket_quota): +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, quota=ticket_quota) + '''.format(account=account_code, quota=ticket_quota) ) if __name__ == '__main__': - for account in conf.accounts: + for code, account in conf.accounts.items(): try: save_check( conf.db_name, - account['account'], + code, get_ticket_quota(account['user'], account['password']) ) - print('{:20} - ok'.format(account['account'])) + print('{:20} - ok'.format(code)) except Exception: - print('{:20} - error'.format(account['account'])) + print('{:20} - error'.format(code)) diff --git a/conf.sample.py b/conf.sample.py index 5c1ff6f..6b30ba6 100644 --- a/conf.sample.py +++ b/conf.sample.py @@ -1,14 +1,19 @@ # -*- coding: utf-8 -*- -db_name = 'db/quota.db' -accounts = [ - { +accounts = { + 'OTA.TCH': { 'user': 'ota_grs101', 'password': 'secret', - 'account': 'OTA.TCH', }, - { + 'OTA.UT': { 'user': 'ota_grs102', 'password': 'newsecret', - 'account': 'OTA.UT', - } -] + }, +} +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/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) From 81043496fc5e469995aa9a270ce958aa202de48c Mon Sep 17 00:00:00 2001 From: Sergey Popinevskiy Date: Mon, 25 Jun 2018 08:37:11 +0300 Subject: [PATCH 4/7] Added logging --- checker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/checker.py b/checker.py index a6d0bd2..65219c8 100644 --- a/checker.py +++ b/checker.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import re import requests +import logging from utils import db_execute import conf @@ -54,5 +55,6 @@ def save_check(db_name, account_code, ticket_quota): get_ticket_quota(account['user'], account['password']) ) print('{:20} - ok'.format(code)) - except Exception: + except Exception as e: print('{:20} - error'.format(code)) + logging.critical(e, exc_info=True) From ecbd600bf79ed9d95fc2dda4ac19fed1cddf7996 Mon Sep 17 00:00:00 2001 From: Sergey Popinevskiy Date: Mon, 2 Jul 2018 21:20:57 +0300 Subject: [PATCH 5/7] Wrote installation manual --- README.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c063d0b..0e618f8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,29 @@ 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. + + +## 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 +``` From 51ce5fcb336de329f4e35e9f8df5dcffc4c0451b Mon Sep 17 00:00:00 2001 From: Sergey Popinevskiy Date: Mon, 2 Jul 2018 21:26:53 +0300 Subject: [PATCH 6/7] Wrote how to run manual --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 0e618f8..82cdf60 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,14 @@ sirena-quota ============ 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 From 3f7340e7d8c502f4139042e69f1932e776aa2199 Mon Sep 17 00:00:00 2001 From: Sergey Popinevskiy Date: Mon, 2 Jul 2018 21:31:08 +0300 Subject: [PATCH 7/7] =?UTF-8?q?Happy=20Birthday=20=E2=9C=A8=F0=9F=8D=B0?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HISTORY.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 1a7a978..45ef1e7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,11 @@ Release History =============== +1.0.0 (2018-07-02) +++++++++++++++++++ + +* Birth! + 0.0.1 (2018-06-23) ++++++++++++++++++