Skip to content

Commit aadd6e0

Browse files
committed
Updated.
1 parent c534bcb commit aadd6e0

5 files changed

Lines changed: 425 additions & 74 deletions

File tree

cbr_ru/xml_metall/common.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
__author__ = 'ipetrash'
5+
6+
7+
import logging
8+
import sys
9+
10+
from pathlib import Path
11+
12+
from config import DIR_LOGS
13+
14+
15+
def get_logger(file_name: str, dir_name='logs'):
16+
dir_name = Path(dir_name).resolve()
17+
dir_name.mkdir(parents=True, exist_ok=True)
18+
19+
file_name = str(dir_name / Path(file_name).resolve().name) + '.log'
20+
21+
log = logging.getLogger(__name__)
22+
log.setLevel(logging.DEBUG)
23+
24+
formatter = logging.Formatter('[%(asctime)s] %(filename)s[LINE:%(lineno)d] %(levelname)-8s %(message)s')
25+
26+
fh = logging.FileHandler(file_name, encoding='utf-8')
27+
fh.setLevel(logging.DEBUG)
28+
29+
ch = logging.StreamHandler(stream=sys.stdout)
30+
ch.setLevel(logging.DEBUG)
31+
32+
fh.setFormatter(formatter)
33+
ch.setFormatter(formatter)
34+
35+
log.addHandler(fh)
36+
log.addHandler(ch)
37+
38+
return log
39+
40+
41+
log = get_logger('main', DIR_LOGS)

cbr_ru/xml_metall/config.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
__author__ = 'ipetrash'
5+
6+
7+
import datetime as DT
8+
import os
9+
import sys
10+
11+
from pathlib import Path
12+
13+
14+
# Текущая папка, где находится скрипт
15+
DIR = Path(__file__).resolve().parent
16+
17+
DIR_LOGS = DIR / 'logs'
18+
DIR_LOGS.mkdir(parents=True, exist_ok=True)
19+
20+
# TOKEN_FILE_NAME = DIR / 'TOKEN.txt'
21+
#
22+
# try:
23+
# TOKEN = os.environ.get('TOKEN') or TOKEN_FILE_NAME.read_text('utf-8').strip()
24+
# if not TOKEN:
25+
# raise Exception('TOKEN пустой!')
26+
#
27+
# except:
28+
# print(f'Нужно в {TOKEN_FILE_NAME.name} или в переменную окружения TOKEN добавить токен бота')
29+
# TOKEN_FILE_NAME.touch()
30+
# sys.exit()
31+
#
32+
# ERROR_TEXT = 'Возникла какая-то проблема. Попробуйте повторить запрос или попробовать чуть позже...'
33+
34+
# Создание папки для базы данных
35+
DB_DIR_NAME = DIR / 'database'
36+
DB_DIR_NAME.mkdir(parents=True, exist_ok=True)
37+
38+
# Путь к файлу базы данных
39+
DB_FILE_NAME = str(DB_DIR_NAME / 'database.sqlite')
40+
41+
USER_NAME_ADMINS = [
42+
'@ilya_petrash',
43+
]
44+
45+
MAX_MESSAGE_LENGTH = 4096
46+
ITEMS_PER_PAGE = 10
47+
48+
START_DATE = DT.date(year=2000, month=1, day=1)

cbr_ru/xml_metall/db.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
__author__ = 'ipetrash'
5+
6+
7+
import datetime as DT
8+
import enum
9+
import time
10+
11+
from decimal import Decimal
12+
from typing import Type, Optional, List, Iterable, Any
13+
14+
# pip install peewee
15+
from peewee import (
16+
Model, TextField, ForeignKeyField, CharField, DecimalField, DateField, Field
17+
)
18+
from playhouse.sqliteq import SqliteQueueDatabase
19+
20+
import parser
21+
22+
from config import DB_FILE_NAME, ITEMS_PER_PAGE, START_DATE
23+
from parser import MetalEnum
24+
25+
26+
# SOURCE: https://github.com/gil9red/SimplePyScripts/blob/cd5bf42742b2de4706a82aecb00e20ca0f043f8e/shorten.py
27+
def shorten(text: str, length=30) -> str:
28+
if not text:
29+
return text
30+
31+
if len(text) > length:
32+
text = text[:length] + '...'
33+
34+
return text
35+
36+
37+
class EnumField(CharField):
38+
"""
39+
This class enable an Enum like field for Peewee
40+
"""
41+
42+
def __init__(self, choices: Type[enum.Enum], *args: Any, **kwargs: Any) -> None:
43+
super().__init__(*args, **kwargs)
44+
45+
self.choices: Type[enum.Enum] = choices
46+
self.max_length = 255
47+
48+
def db_value(self, value: Any) -> Any:
49+
return value.value
50+
51+
def python_value(self, value: Any) -> Any:
52+
type_value_enum = type(list(self.choices)[0].value)
53+
value_enum = type_value_enum(value)
54+
return self.choices(value_enum)
55+
56+
57+
# This working with multithreading
58+
# SOURCE: http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#sqliteq
59+
db = SqliteQueueDatabase(
60+
DB_FILE_NAME,
61+
pragmas={
62+
'foreign_keys': 1,
63+
'journal_mode': 'wal', # WAL-mode
64+
'cache_size': -1024 * 64 # 64MB page-cache
65+
},
66+
use_gevent=False, # Use the standard library "threading" module.
67+
autostart=True,
68+
queue_max_size=64, # Max. # of pending writes that can accumulate.
69+
results_timeout=5.0 # Max. time to wait for query to be executed.
70+
)
71+
72+
73+
class BaseModel(Model):
74+
"""
75+
Базовая модель для классов-таблиц
76+
"""
77+
78+
class Meta:
79+
database = db
80+
81+
def get_new(self) -> Type['BaseModel']:
82+
return type(self).get(self._pk_expr())
83+
84+
@classmethod
85+
def get_first(cls) -> Type['BaseModel']:
86+
return cls.select().first()
87+
88+
@classmethod
89+
def get_last(cls) -> Type['BaseModel']:
90+
return cls.select().order_by(cls.id.desc()).first()
91+
92+
@classmethod
93+
def paginating(
94+
cls,
95+
page: int = 1,
96+
items_per_page: int = ITEMS_PER_PAGE,
97+
order_by: Field = None,
98+
filters: Iterable = None,
99+
) -> List[Type['BaseModel']]:
100+
query = cls.select()
101+
102+
if filters:
103+
query = query.filter(*filters)
104+
105+
if order_by:
106+
query = query.order_by(order_by)
107+
108+
query = query.paginate(page, items_per_page)
109+
return list(query)
110+
111+
@classmethod
112+
def get_inherited_models(cls) -> List[Type['BaseModel']]:
113+
return sorted(cls.__subclasses__(), key=lambda x: x.__name__)
114+
115+
@classmethod
116+
def print_count_of_tables(cls):
117+
items = []
118+
for sub_cls in cls.get_inherited_models():
119+
name = sub_cls.__name__
120+
count = sub_cls.select().count()
121+
items.append(f'{name}: {count}')
122+
123+
print(', '.join(items))
124+
125+
def __str__(self):
126+
fields = []
127+
for k, field in self._meta.fields.items():
128+
v = getattr(self, k)
129+
130+
if isinstance(field, (TextField, CharField)):
131+
if v:
132+
if isinstance(v, enum.Enum):
133+
v = v.value
134+
135+
v = repr(shorten(v))
136+
137+
elif isinstance(field, ForeignKeyField):
138+
k = f'{k}_id'
139+
if v:
140+
v = v.id
141+
142+
fields.append(f'{k}={v}')
143+
144+
return self.__class__.__name__ + '(' + ', '.join(fields) + ')'
145+
146+
147+
class MetalRate(BaseModel):
148+
date = DateField()
149+
metal = EnumField(choices=MetalEnum)
150+
amount = DecimalField(decimal_places=2)
151+
152+
class Meta:
153+
indexes = (
154+
(('date', 'metal'), True),
155+
)
156+
157+
@classmethod
158+
def get_by(cls, date: DT.date, metal: MetalEnum) -> Optional['MetalRate']:
159+
return cls.get_or_none(date=date, metal=metal)
160+
161+
@classmethod
162+
def add(cls, date: DT.date, metal: MetalEnum, amount: Decimal) -> 'MetalRate':
163+
obj = cls.get_by(date, metal)
164+
if not obj:
165+
obj = cls.create(
166+
date=date,
167+
metal=metal,
168+
amount=amount,
169+
)
170+
171+
return obj
172+
173+
@classmethod
174+
def add_from(cls, metal_rate: parser.MetalRate) -> 'MetalRate':
175+
return cls.add(metal_rate.date, metal_rate.metal, metal_rate.amount)
176+
177+
@classmethod
178+
def get_last_date(cls) -> DT.date:
179+
obj = cls.select(cls.date).order_by(cls.date.desc()).first()
180+
return obj.date if obj else START_DATE
181+
182+
183+
db.connect()
184+
db.create_tables(BaseModel.get_inherited_models())
185+
186+
# Задержка в 50мс, чтобы дать время на запуск SqliteQueueDatabase и создание таблиц
187+
# Т.к. в SqliteQueueDatabase запросы на чтение выполняются сразу, а на запись попадают в очередь
188+
time.sleep(0.050)
189+
190+
if __name__ == '__main__':
191+
BaseModel.print_count_of_tables()
192+
# MetalRate: 0
193+
print()
194+
195+
print(MetalRate.get_last_date())

0 commit comments

Comments
 (0)