Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
rostyq committed Nov 13, 2023
1 parent e797765 commit d9e3af9
Show file tree
Hide file tree
Showing 20 changed files with 283 additions and 318 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
__pycache__/
/venv/
.env
.env
.env.*
87 changes: 7 additions & 80 deletions liqpy/api.py → liqpy/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,32 @@
from typing import TYPE_CHECKING, Any, AnyStr, Optional, Unpack

from functools import singledispatchmethod
from enum import Enum
from dataclasses import asdict

from urllib.parse import urljoin
from base64 import b64encode, b64decode
from hashlib import sha1
from json import loads, JSONEncoder

from uuid import UUID
from decimal import Decimal
from datetime import date, datetime, UTC
from datetime import datetime, UTC

from .data import FiscalItem, DetailAddenda, SplitRule
from .encoder import Encoder, JSONEncoder
from .preprocess import Preprocessor, BasePreprocessor
from .validation import Validator, BaseValidator
from .exceptions import exception

if TYPE_CHECKING:
from requests import Session, Response

from .types import LiqpayRequestDict
from .types.action import Action
from .types.post import Hooks, Proxies, Timeout, Verify, Cert
from liqpy.types import LiqpayRequestDict
from liqpy.types.action import Action
from liqpy.types.post import Hooks, Proxies, Timeout, Verify, Cert


__all__ = ("Endpoint", "post", "sign", "encode", "decode", "request")

URL = "https://www.liqpay.ua"
VERSION = 3

SENDER_KEYS = {
"sender_first_name",
"sender_last_name",
"sender_email",
"sender_address",
"sender_city",
"sender_country_code",
"sender_postal_code",
"sender_shipping_state",
}

PRODUCT_KEYS = {
"product_category",
"product_description",
"product_name",
"product_url",
}


class Endpoint(Enum):
REQUEST: str = "/api/request"
Expand All @@ -57,58 +36,6 @@ def url(self) -> str:
return urljoin(URL, self.value)


class LiqPayJSONEncoder(JSONEncoder):
date_fmt = r"%Y-%m-%d %H:%M:%S"

def __init__(self) -> None:
super().__init__(
skipkeys=False,
ensure_ascii=True,
check_circular=True,
allow_nan=False,
sort_keys=False,
indent=None,
separators=None,
default=None,
)

@singledispatchmethod
def default(self, o):
return super().default(o)

@default.register
def _(self, o: Decimal) -> float:
return round(float(o), 4)

@default.register
def _(self, o: datetime) -> str:
return o.astimezone(UTC).strftime(self.date_fmt)

@default.register
def _(self, o: date) -> str:
return o.strftime(self.date_fmt)

@default.register
def _(self, o: bytes) -> str:
return o.decode("utf-8")

@default.register
def _(self, o: UUID) -> str:
return str(o)

@default.register
def _(self, o: DetailAddenda) -> str:
return b64encode(self.encode(o.to_dict()).encode()).decode()

@default.register
def _(self, o: SplitRule) -> dict:
return asdict(o)

@default.register
def _(self, o: FiscalItem) -> dict:
return asdict(o)


def is_sandbox(key: str, /) -> bool:
return key.startswith("sandbox_")

Expand Down Expand Up @@ -215,7 +142,7 @@ def encode(
params = {key: value for key, value in params.items() if value is not None}

if encoder is None:
encoder = LiqPayJSONEncoder()
encoder = Encoder()

if preprocessor is None:
preprocessor = Preprocessor()
Expand Down
41 changes: 41 additions & 0 deletions liqpy/api/decoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from json import JSONDecoder
from ipaddress import IPv4Address

from liqpy.util.convert import to_datetime


class Decoder(JSONDecoder):
def __init__(self):
super().__init__(
object_hook=self._object_hook,
parse_float=float,
parse_int=int,
parse_constant=None,
strict=True,
object_pairs_hook=None,
)
self.create_date = to_datetime
self.end_date = to_datetime
self.completion_date = to_datetime
self.mpi_eci = int
self.ip = IPv4Address
self.refund_date_last = to_datetime

def _object_hook(self, o: dict, /) -> dict:
for key, value in o.items():
try:
fn = getattr(self, key, None)

if not callable(fn):
continue

processed = fn(value)

if processed is not None:
o[key] = processed

except Exception as e:
raise Exception(f"Failed to post convert {key} parameter.") from e

return o

76 changes: 76 additions & 0 deletions liqpy/api/encoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from functools import singledispatchmethod
from dataclasses import asdict

from base64 import b64encode
from json import JSONEncoder

from uuid import UUID
from decimal import Decimal
from datetime import date, datetime, UTC

from liqpy.models.request import FiscalItem, DetailAddenda, SplitRule


__all__ = ("Encoder", "JSONEncoder")


class Encoder(JSONEncoder):
date_fmt = r"%Y-%m-%d %H:%M:%S"

def __init__(self) -> None:
super().__init__(
skipkeys=False,
ensure_ascii=True,
check_circular=True,
allow_nan=False,
sort_keys=False,
indent=None,
separators=None,
default=None,
)

@singledispatchmethod
def default(self, o):
return super().default(o)

@default.register
def _(self, o: Decimal) -> float:
return round(float(o), 4)

@default.register
def _(self, o: datetime) -> str:
return o.astimezone(UTC).strftime(self.date_fmt)

@default.register
def _(self, o: date) -> str:
return o.strftime(self.date_fmt)

@default.register
def _(self, o: bytes) -> str:
return o.decode("utf-8")

@default.register
def _(self, o: UUID) -> str:
return str(o)

@default.register
def _(self, o: DetailAddenda) -> str:
data = {
"airLine": o.air_line,
"ticketNumber": o.ticket_number,
"passengerName": o.passenger_name,
"flightNumber": o.flight_number,
"originCity": o.origin_city,
"destinationCity": o.destination_city,
"departureDate": o.departure_date.strftime(r"%d%m%y"),
}

return b64encode(self.encode(data).encode()).decode()

@default.register
def _(self, o: SplitRule) -> dict:
return asdict(o)

@default.register
def _(self, o: FiscalItem) -> dict:
return asdict(o)
4 changes: 2 additions & 2 deletions liqpy/exceptions.py → liqpy/api/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
if TYPE_CHECKING:
from requests import Response

from .types.error import (
from liqpy.types.error import (
LiqPayErrcode,
LiqpayAntiFraudErrcode,
LiqpayFinancialErrcode,
Expand Down Expand Up @@ -97,7 +97,7 @@ def get_exception_cls(code: str | None = None) -> type[LiqPayException]:
return LiqPayException


def exception_factory(
def exception(
code: str | None = None,
description: str | None = None,
*,
Expand Down
8 changes: 4 additions & 4 deletions liqpy/preprocess.py → liqpy/api/preprocess.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from typing import TYPE_CHECKING, Optional
from datetime import timedelta

from .convert import to_datetime, to_milliseconds
from .data import DetailAddenda
from liqpy.models.request import DetailAddenda
from liqpy.util.convert import to_datetime, to_milliseconds

if TYPE_CHECKING:
from json import JSONEncoder
from .types.request import LiqpayRequestDict

from liqpy.types.request import LiqpayRequestDict


class BasePreprocessor:
Expand Down
50 changes: 12 additions & 38 deletions liqpy/validation.py → liqpy/api/validation.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from typing import TYPE_CHECKING
from functools import cache
from datetime import datetime
from re import compile
from re import fullmatch
from numbers import Number
from uuid import UUID
from urllib.parse import urlparse

from .data import DetailAddenda, SplitRule, FiscalItem, FiscalInfo
from liqpy.models.request import DetailAddenda, SplitRule, FiscalItem, FiscalInfo

if TYPE_CHECKING:
from .types.request import (
from liqpy.types.request import (
DetailAddendaDict,
SplitRuleDict,
FiscalItemDict,
Expand All @@ -18,31 +17,6 @@
)


@cache
def phone_pattern():
return compile(r"\+?380\d{9}")


@cache
def card_cvv_pattern():
return compile(r"\d{3}")


@cache
def card_number_pattern():
return compile(r"\d{16}")


@cache
def card_exp_year_pattern():
return compile(r"(\d{2})?\d{2}")


@cache
def card_exp_month_pattern():
return compile(r"(0[1-9])|(1[0-2])")


def noop(value, /, **kwargs):
pass

Expand All @@ -63,7 +37,7 @@ def string(value, /, *, max_len: int | None = None):


def url(value, /, *, max_len: int | None = None):
string(value, max_len=max_len)
string(value, max_len=max_len)
result = urlparse(value or "")
assert result.scheme in (
"http",
Expand Down Expand Up @@ -141,8 +115,8 @@ def resp_format(self, value, /, **kwargs):
), "format must be json, csv or xml"

def phone(self, value, /, **kwargs):
assert phone_pattern().fullmatch(
value
assert fullmatch(
r"\+?380\d{9}", value
), "phone must be in format +380XXXXXXXXX or 380XXXXXXXXX"

def sender_phone(self, value, /, **kwargs):
Expand All @@ -155,19 +129,19 @@ def language(self, value, /, **kwargs):
assert value in ("uk", "en"), "language must be uk or en"

def card_number(self, value, /, **kwargs):
assert card_number_pattern().fullmatch(value), f"card must be 16 digits long"
assert fullmatch(r"\d{16}", value), f"card must be 16 digits long"

def card_cvv(self, value, /, **kwargs):
assert card_cvv_pattern().fullmatch(value), f"cvv must be 3 digits long"
assert fullmatch(r"\d{3}", value), f"cvv must be 3 digits long"

def card_exp_year(self, value, /, **kwargs):
assert card_exp_year_pattern().fullmatch(
value
assert fullmatch(
r"(\d{2})?\d{2}", value
), f"exp_year must be 2 or 4 digits long"

def card_exp_month(self, value, /, **kwargs):
assert card_exp_month_pattern().fullmatch(
value
assert fullmatch(
r"(0[1-9])|(1[0-2])", value
), f"exp_month must be 2 digits long and between 01 and 12"

def subscribe(self, value, /, **kwargs):
Expand Down
Loading

0 comments on commit d9e3af9

Please sign in to comment.