PK!alfa_orders/__init__.pyPK!alfa_orders/api.pyimport datetime as dt from typing import Iterable, List from alfa_orders.config import AlfaConfig from alfa_orders.loaders.refunds import Refund, RefundLoader from alfa_orders.loaders.transactions import TransactionLoader, Transaction, TransactionStatus from alfa_orders.session import create_alfa_session class AlfaService: """ Usage: >>> username, password = ("**********", "**********") >>> service = AlfaService(username, password) >>> from_date, to_date = dt.datetime(2019, 9, 1), dt.datetime(2019, 10, 1) >>> transactions = list(service.get_transactions(from_date, to_date)) >>> refunds = list(service.get_refunds(from_date, to_date)) """ def __init__(self, username: str, password: str, config: AlfaConfig = None) -> None: self.username = username self.password = password self.config = config or AlfaConfig() if self.config.LAZY_SESSION_INIT: self.session = None else: self.session = self.init_session() def init_session(self): self.session = create_alfa_session(self.username, self.password, self.config) return self.session def get_transactions( self, from_date: dt.datetime, to_date: dt.datetime, statuses: List[TransactionStatus] = None ) -> Iterable[Transaction]: assert self.session return TransactionLoader(self.session, self.config, statuses)(from_date, to_date) def get_refunds(self, from_date: dt.datetime, to_date: dt.datetime) -> Iterable[Refund]: assert self.session return RefundLoader(self.session, self.config)(from_date, to_date) PK!hA{ { alfa_orders/columns.pyTransactionColumns = { "orderNumber": "Номер заказа", "registerDate": "Дата", "ip": "IP-адрес", "mdOrder": "Уникальный номер заказа в системе", "paymentState": "Состояние", "merchantFullName": "Название продавца", "merchantLogin": "Логин продавца", "orderDescription": "Описание заказа", "amount": "Сумма", "paymentDate": "Дата оплаты", "depositedDate": "Дата списания", "refundedDate": "Дата последнего возврата", "reversedDate": "Дата отмены", "feeAmount": "Сумма комиссии", "currency": "Валюта", "approvedAmount": "Сумма предавторизации", "depositedAmount": "Сумма подтверждения", "refundedAmount": "Сумма возврата", "fraudWeight": "Вес фрода", "paymentWay": "Платежное средство", "cardholderName": "Имя владельца карты", "pan": "Номер карты", "expiry": "Дата окончания действия", "paymentSystem": "Платежная система", "product": "Продукт", "bankName": "Название Банка", "panCountryCode": "Код страны банка", "ipCountryCode": "Код страны IP-адреса", "panCountry": "Страна банка", "ipCountry": "Страна IP-адреса", "actionCode": "Код ответа процессинга", "actionCodeDescription": "Описание кода ответа процессинга", "originalActionCode": "Оригинальный код ответа процессинга", "approvalCode": "Approval Code", "authCode": "Auth code", "referenceNumber": "Reference number", "authReferenceNumber": "Authentication reference number", "terminalId": "Терминал", "processingId": "Процессинг", "transactionDesc": "3DSec/SSL", "eci": "ECI", "clientId": "Идентификатор клиента", "phone": "Телефон клиента", "email": "Email клиента", "orderParamsAsString": "Дополнительные параметры заказа", # extra columns "paymentStateDescription": "Описание состояния", } RefundColumns = { "id": "ID", "refundDate": "Дата возврата", "paymentDate": "Дата платежа", "merchantFullName": "Название продавца", "mdOrder": "ID заказа", "orderNumber": "Номер заказа", "amount": "Сумма возврата", "maskedPan": "Номер карты", "refundState": "Состояние возврата", "currencyName": "Валюта", "cardholder": "Держатель карты", "bankName": "Банк-эмитент", "countryName": "Страна банка-эмитента", "actionCode": "Код результата", "refNum": "Reference number", "processingId": "ID процессинга", "terminalId": "ID терминала", "paymentSystem": "МПС", "merchantMcc": "MCC" } PK!\^alfa_orders/config.pyfrom dataclasses import dataclass @dataclass class AlfaConfig: HOST: str = "https://engine.paymentgate.ru" RETRY_SECONDS: int = 60 PAGE_SIZE: int = 100 MAX_FUTURES: int = 10 UTC_3_SEARCH: bool = True PARSE_TIMESTAMP: bool = False PARSE_TIMESTAMP_AS_UTC3: bool = False PARSE_AMOUNT: bool = False MAP_RUSSIAN_COLUMNS: bool = False MAP_REFUND_STATUS: bool = False LAZY_SESSION_INIT: bool = False PK!alfa_orders/loaders/__init__.pyPK!Ktalfa_orders/loaders/base.pyimport datetime as dt from abc import abstractmethod from itertools import count from typing import Generic, Iterable import requests from more_itertools import flatten, chunked from alfa_orders.config import AlfaConfig from alfa_orders.models import AlfaFuture, T class BaseLoader(Generic[T]): def __init__(self, session: requests.Session, config: AlfaConfig): self.session = session self.config = config self.post_processors = [] if config.PARSE_TIMESTAMP: from alfa_orders.processors import TimestampProcessor self.post_processors.append(TimestampProcessor(self, self.config)) if config.PARSE_AMOUNT: from alfa_orders.processors import ParseAmountProcessor self.post_processors.append(ParseAmountProcessor(self, self.config)) # should be in the end if config.MAP_RUSSIAN_COLUMNS: from alfa_orders.processors import RussianColumnProcessor self.post_processors.append(RussianColumnProcessor(self, self.config)) def __call__(self, from_date, to_date): yield from self._load(from_date, to_date) @property @abstractmethod def columns(self): pass def _load(self, from_date, to_date): futures = (self._get_future(from_date, to_date, offset) for offset in count(0, self.config.PAGE_SIZE)) future_batches = chunked(futures, self.config.MAX_FUTURES) order_batches = ( tuple(flatten(map(self._post_process, map(self._get_by_future, batch)))) for batch in future_batches ) for batch in order_batches: yield from batch if len(batch) < self.config.PAGE_SIZE * self.config.MAX_FUTURES: break @abstractmethod def _get_future(self, from_date: dt.datetime, to_date: dt.datetime, offset: int = 0) -> AlfaFuture: pass @abstractmethod def _get_by_future(self, future: AlfaFuture) -> Iterable[T]: pass def _post_process(self, orders: Iterable[T]) -> Iterable[T]: for proc in self.post_processors: orders = proc(orders) return orders PK!YYalfa_orders/loaders/refunds.pyimport datetime as dt import time from typing import Dict, Iterable from alfa_orders.columns import RefundColumns from alfa_orders.loaders.base import BaseLoader from alfa_orders.models import AlfaFuture, AlfaFutureResult from alfa_orders.utils import timestamp_now Refund = Dict RefundStatuses = { "POSTED": "Отправлен" } class RefundLoader(BaseLoader[Refund]): DATETIME_FORMAT = "%d.%m.%Y %H:%M" columns = RefundColumns def _get_future(self, from_date: dt.datetime, to_date: dt.datetime, offset: int = 0) -> AlfaFuture: shift = dt.timedelta(hours=3 if self.config.UTC_3_SEARCH else 0) alfa_from_date = (from_date - shift).strftime(self.DATETIME_FORMAT) alfa_to_date = (to_date - shift).strftime(self.DATETIME_FORMAT) data = { 'page': '1', 'start': offset, 'limit': self.config.PAGE_SIZE, 'sort': 'refundDate', 'dir': 'DESC', 'dateFrom': alfa_from_date, 'dateTo': alfa_to_date, } response = self.session.post( f"{self.config.HOST}/mportal/mvc/refunds/search", params={"_dc": timestamp_now()}, data=data ) return response.json() def _get_by_future(self, future: AlfaFuture) -> Iterable[Refund]: url = f"{self.config.HOST}/mportal/mvc/refunds/search" params = {"_dc": timestamp_now()} data = future for _ in range(self.config.RETRY_SECONDS): resp = self.session.post(url, data, params=params) resp_json: AlfaFutureResult = resp.json() if resp_json["done"]: return tuple(map(self.__map_refund_status, resp_json["data"])) time.sleep(1) else: raise LookupError(f"No orders for future {future['future']} in {self.config.RETRY_SECONDS} seconds") def __map_refund_status(self, refund: Dict) -> Dict: if self.config.MAP_REFUND_STATUS: refund["refundState"] = RefundStatuses[refund["refundState"]] return refund PK!Xo #alfa_orders/loaders/transactions.pyimport datetime as dt import time from enum import Enum from typing import Dict, List import requests from alfa_orders.columns import TransactionColumns from alfa_orders.config import AlfaConfig from alfa_orders.loaders.base import BaseLoader from alfa_orders.models import AlfaFutureResult, AlfaFuture from alfa_orders.utils import timestamp_now # see data/transaction.json and data/transaction_columns.json for order fields Transaction = Dict class TransactionStatus(Enum): DEPOSITED = "DEPOSITED" REFUNDED = "REFUNDED" class TransactionLoader(BaseLoader[Transaction]): DATETIME_FORMAT = "%d.%m.%Y %H:%M:%S" columns = TransactionColumns def __init__(self, session: requests.Session, config: AlfaConfig, statuses: List[TransactionStatus] = None): super().__init__(session, config) self.statuses: List[TransactionStatus] = statuses or [TransactionStatus.DEPOSITED, TransactionStatus.REFUNDED] def _get_future(self, from_date: dt.datetime, to_date: dt.datetime, offset: int = 0) -> AlfaFuture: shift = dt.timedelta(hours=3 if self.config.UTC_3_SEARCH else 0) alfa_from_date = (from_date - shift).strftime(self.DATETIME_FORMAT) alfa_to_date = (to_date - shift).strftime(self.DATETIME_FORMAT) url = f"{self.config.HOST}/mportal/mvc/transaction" params = {"_dc": timestamp_now()} data = { "start": offset, "limit": self.config.PAGE_SIZE, "dateFrom": alfa_from_date, "dateTo": alfa_to_date, "orderStateStr": ",".join(str(status.value) for status in self.statuses), "page": '1', "dateMode": "CREATION_DATE", "merchants": "", } response = self.session.post(url, data, params=params) response.raise_for_status() response_json = response.json() return response_json def _get_by_future(self, future: AlfaFuture): url = f"{self.config.HOST}/mportal/mvc/transaction" params = {"_dc": timestamp_now()} data = future for _ in range(self.config.RETRY_SECONDS): resp = self.session.post(url, data, params=params) resp_json: AlfaFutureResult = resp.json() if resp_json["done"]: return resp_json["data"] time.sleep(1) else: raise LookupError(f"No orders for future {future['future']} in {self.config.RETRY_SECONDS} seconds") PK!.??alfa_orders/models.pyfrom typing import TypeVar, List, Dict from typing_extensions import TypedDict T = TypeVar("T") Offset = int class AlfaFuture(TypedDict): # {'future': 'ea592611-558a-47ff-acd1-0f8a2f3e3ed0'} future: str class AlfaFutureResult(TypedDict): done: bool success: bool data: List[Dict]PK!oalfa_orders/processors.pyfrom abc import abstractmethod from typing import Iterable, Dict from alfa_orders.config import AlfaConfig from alfa_orders.loaders.base import BaseLoader from alfa_orders.utils import parse_timestamp class BaseProcessor: def __init__(self, loader: BaseLoader, config: AlfaConfig): self.loader = loader self.config = config @abstractmethod def __call__(self, orders: Iterable[Dict]) -> Iterable[Dict]: pass class TimestampProcessor(BaseProcessor): def __call__(self, orders: Iterable[Dict]) -> Iterable[Dict]: return self._parse_timestamps(orders) def _parse_timestamps(self, orders: Iterable[Dict]) -> Iterable[Dict]: for order in orders: yield { **order, **{ field: parse_timestamp(value, self.config.PARSE_TIMESTAMP_AS_UTC3) for field, value in order.items() if "Date" in field } } class RussianColumnProcessor(BaseProcessor): def __call__(self, orders: Iterable[Dict]) -> Iterable[Dict]: for order in orders: yield {self.loader.columns.get(field, field): value for field, value in order.items()} class ParseAmountProcessor(BaseProcessor): def __call__(self, orders: Iterable[Dict]) -> Iterable[Dict]: for order in orders: yield { **order, **{ # '3500,00' > 3500 field: float(value.replace(",", ".")) if value else value for field, value in order.items() if "amount" in field.lower() } }PK!nQHalfa_orders/session.pyimport requests def create_alfa_session(username, password, config): session = requests.Session() session.headers = { "Accept-Language": "ru,en;q=0.8", "Cache-Control": "no-cache", "Pragma": "no-cache", "User-Agent": "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.97 " "YaBrowser/15.9.2403.2152 (beta) Safari/537.36", "X-Requested-With": "XMLHttpRequest", "Referer": f"{config.HOST}/mportal/", "Origin": f"{config.HOST}" } response = session.post( f"{config.HOST}/mportal/login", {"username": username, "password": password} ) response.raise_for_status() return sessionPK! tMalfa_orders/utils.pyimport datetime as dt from typing import Optional def timestamp_now() -> int: return int(dt.datetime.now().timestamp()) def parse_timestamp(timestamp: Optional[int], as_utc3: bool = False) -> Optional[dt.datetime]: if not timestamp: return timestamp datetime = dt.datetime.utcfromtimestamp(timestamp / 1000) if as_utc3: datetime += dt.timedelta(hours=3) return datetime PK!HڽTU!alfa_orders-0.3.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!Hr$alfa_orders-0.3.0.dist-info/METADATARMo0 W^m@6)֥˲bmcƌ#ĒQ&@F%hr?k1 #Yd$K*+2U6SYdhix9~~e`Tzr ]Lro+@C9dxO' b ;( hu,[m .sCӑ8ihOAI mFW65uMd3ZY -q,Ta*S^h(iVcu+~p#&y[ w:;=A}6XUgLf験h t8+]%\YްTN^5ӣIqsup1{Lm`ikaUn4Ŵ >5đ/ zZ}Yy4\V&8kPBoxZNI1jYK%ƁF b;4*gO!_zAٳy`GC_qZc^"G )t ϶: =Ky{V).jнϟ7&$(c`_n@9 [N'o^1 ]?ϨOfA ,"OcW?|y_BRW rh F,09!m)mf9Yz3^=]J#A=_XzkFɪ{l SzoT5ȚAɛL{<\UZzf 衬ɡEFgBgLb$TvuJ Iwf_W]_#oPK!alfa_orders/__init__.pyPK!5alfa_orders/api.pyPK!hA{ { alfa_orders/columns.pyPK!\^alfa_orders/config.pyPK!alfa_orders/loaders/__init__.pyPK!Ktalfa_orders/loaders/base.pyPK!YYalfa_orders/loaders/refunds.pyPK!Xo #R'alfa_orders/loaders/transactions.pyPK!.??y1alfa_orders/models.pyPK!o2alfa_orders/processors.pyPK!nQH9alfa_orders/session.pyPK! tM=alfa_orders/utils.pyPK!HڽTU!>alfa_orders-0.3.0.dist-info/WHEELPK!Hr$?alfa_orders-0.3.0.dist-info/METADATAPK!H)뽕"Aalfa_orders-0.3.0.dist-info/RECORDPK@D