ebarimt-pos-sdk 0.0.1b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,41 @@
1
+ from .client import PosApiClient
2
+ from .resources.receipt import (
3
+ BarCodeType,
4
+ CreateReceiptRequest,
5
+ CreateReceiptResponse,
6
+ DeleteReceiptRequest,
7
+ DeleteReceiptResponse,
8
+ Item,
9
+ Payment,
10
+ PaymentCardData,
11
+ PaymentCode,
12
+ PaymentStatus,
13
+ Receipt,
14
+ ReceiptCreateStatus,
15
+ ReceiptItemData,
16
+ ReceiptItemResponse,
17
+ ReceiptType,
18
+ TaxType,
19
+ )
20
+ from .settings import PosApiSettings
21
+
22
+ __all__ = [
23
+ "PosApiClient",
24
+ "PosApiSettings",
25
+ "BarCodeType",
26
+ "CreateReceiptRequest",
27
+ "CreateReceiptResponse",
28
+ "DeleteReceiptRequest",
29
+ "DeleteReceiptResponse",
30
+ "Item",
31
+ "Payment",
32
+ "PaymentCardData",
33
+ "PaymentCode",
34
+ "PaymentStatus",
35
+ "Receipt",
36
+ "ReceiptCreateStatus",
37
+ "ReceiptItemData",
38
+ "ReceiptItemResponse",
39
+ "ReceiptType",
40
+ "TaxType",
41
+ ]
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from types import TracebackType
4
+
5
+ import httpx
6
+
7
+ from .resources import BankAccountsResource, InfoResource, ReceiptResource, SendDataResource
8
+ from .settings import PosApiSettings
9
+ from .transport import AsyncTransport, HeaderTypes, SyncTransport
10
+
11
+
12
+ class PosApiClient:
13
+ """
14
+ Dual sync/async client.
15
+
16
+ Usage:
17
+ client = PosApiClient(PosApiSettings(...))
18
+ resp = client.receipt.create({...})
19
+
20
+ async with PosApiClient(...) as client:
21
+ resp = await client.receipt.acreate({...})
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ settings: PosApiSettings,
27
+ *,
28
+ sync_client: httpx.Client | None = None,
29
+ async_client: httpx.AsyncClient | None = None,
30
+ headers: HeaderTypes | None = None,
31
+ ) -> None:
32
+ self._settings = settings
33
+ self._headers = headers
34
+
35
+ self._owns_sync = sync_client is None
36
+ self._owns_async = async_client is None
37
+
38
+ self._base_url = self._settings.base_url
39
+
40
+ self._sync_client = sync_client or httpx.Client(
41
+ base_url=self._base_url,
42
+ timeout=settings.timeout_s,
43
+ verify=settings.verify_tls,
44
+ )
45
+ self._async_client = async_client or httpx.AsyncClient(
46
+ base_url=self._base_url,
47
+ timeout=settings.timeout_s,
48
+ verify=settings.verify_tls,
49
+ )
50
+
51
+ self._sync_transport = SyncTransport(self._sync_client)
52
+ self._async_transport = AsyncTransport(self._async_client)
53
+
54
+ # Resources
55
+ self.receipt = ReceiptResource(
56
+ sync=self._sync_transport,
57
+ async_=self._async_transport,
58
+ headers=self._headers,
59
+ )
60
+ self.info = InfoResource(
61
+ sync=self._sync_transport,
62
+ async_=self._async_transport,
63
+ headers=self._headers,
64
+ )
65
+ self.send_data = SendDataResource(
66
+ sync=self._sync_transport,
67
+ async_=self._async_transport,
68
+ headers=self._headers,
69
+ )
70
+ self.bank_accounts = BankAccountsResource(
71
+ sync=self._sync_transport,
72
+ async_=self._async_transport,
73
+ headers=self._headers,
74
+ )
75
+
76
+ def close(self) -> None:
77
+ if self._owns_sync:
78
+ self._sync_client.close()
79
+
80
+ async def aclose(self) -> None:
81
+ if self._owns_async:
82
+ await self._async_client.aclose()
83
+
84
+ async def __aenter__(self) -> PosApiClient:
85
+ return self
86
+
87
+ async def __aexit__(
88
+ self,
89
+ exc_type: type[BaseException] | None,
90
+ exc: BaseException | None,
91
+ tb: TracebackType | None,
92
+ ) -> None:
93
+ await self.aclose()
94
+
95
+ def __enter__(self) -> PosApiClient:
96
+ return self
97
+
98
+ def __exit__(
99
+ self,
100
+ exc_type: type[BaseException] | None,
101
+ exc: BaseException | None,
102
+ tb: TracebackType | None,
103
+ ) -> None:
104
+ self.close()
@@ -0,0 +1,65 @@
1
+ """Ebarimt Pos API sdk errors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+
8
+ class PosApiError(Exception):
9
+ """Base error for the SDK."""
10
+
11
+ def __init__(
12
+ self,
13
+ message: str,
14
+ *,
15
+ request: httpx.Request | None = None,
16
+ response: httpx.Response | None = None,
17
+ ) -> None:
18
+ super().__init__(message)
19
+ self.request = request
20
+ self.response = response
21
+
22
+
23
+ class PosApiTransportError(PosApiError):
24
+ """Network / timeout / DNS / TLS errors."""
25
+
26
+
27
+ class PosApiDecodeError(PosApiError):
28
+ """Response body was not valid JSON when JSON was expected."""
29
+
30
+
31
+ class PosApiValidationError(PosApiError):
32
+ """Pydantic validation errors (request or response)."""
33
+
34
+
35
+ class PosApiHttpError(PosApiError):
36
+ """Non-2xx response from server."""
37
+
38
+ def __init__(
39
+ self,
40
+ message: str,
41
+ *,
42
+ request: httpx.Request,
43
+ response: httpx.Response,
44
+ ) -> None:
45
+ super().__init__(message, request=request, response=response)
46
+
47
+
48
+ class PosApiBusinessError(PosApiError):
49
+ """
50
+ 2xx HTTP but domain-level failure indicated by payload fields
51
+ (e.g. {"status":"ERROR", "message":"..."}).
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ message: str,
57
+ *,
58
+ status: str | None = None,
59
+ code: str | int | None = None,
60
+ request: httpx.Request | None = None,
61
+ response: httpx.Response | None = None,
62
+ ) -> None:
63
+ super().__init__(message, request=request, response=response)
64
+ self.status = status
65
+ self.code = code
File without changes
@@ -0,0 +1,6 @@
1
+ from .bank_accounts.bank_accounts import BankAccountsResource
2
+ from .info.info import InfoResource
3
+ from .receipt.receipt import ReceiptResource
4
+ from .send_data.send_data import SendDataResource
5
+
6
+ __all__ = ["InfoResource", "ReceiptResource", "SendDataResource", "BankAccountsResource"]
@@ -0,0 +1,54 @@
1
+ import httpx
2
+
3
+ from ..resource import BaseResource, HeaderTypes, _build_headers, _ensure_http_success
4
+ from .schema import BankAccount
5
+
6
+
7
+ class BankAccountsResource(BaseResource):
8
+ @property
9
+ def _path(self) -> str:
10
+ return "/rest/bankAccounts"
11
+
12
+ def read(
13
+ self,
14
+ tin: str,
15
+ *,
16
+ headers: HeaderTypes | None = None,
17
+ ) -> list[BankAccount]:
18
+ result = self._sync.send(
19
+ "GET",
20
+ self._path,
21
+ params=httpx.QueryParams({"tin": tin}),
22
+ headers=_build_headers(self._headers, headers),
23
+ )
24
+
25
+ success_response = _ensure_http_success(result.response)
26
+
27
+ output: list[BankAccount] = []
28
+
29
+ for data in success_response.json():
30
+ output.append(BankAccount.model_validate(data))
31
+
32
+ return output
33
+
34
+ async def aread(
35
+ self,
36
+ tin: str,
37
+ *,
38
+ headers: HeaderTypes | None = None,
39
+ ) -> list[BankAccount]:
40
+ result = await self._async.send(
41
+ "GET",
42
+ self._path,
43
+ params=httpx.QueryParams({"tin": tin}),
44
+ headers=_build_headers(self._headers, headers),
45
+ )
46
+
47
+ success_response = _ensure_http_success(result.response)
48
+
49
+ output: list[BankAccount] = []
50
+
51
+ for data in success_response.json():
52
+ output.append(BankAccount.model_validate(data))
53
+
54
+ return output
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class SDKModel(BaseModel):
9
+ """
10
+ Base model for the SDK:
11
+ - Allows forward/backward-compatible payload changes (extra keys allowed)
12
+ - Clean developer ergonomics (aliases supported)
13
+ """
14
+
15
+ model_config = ConfigDict(
16
+ extra="allow",
17
+ populate_by_name=True,
18
+ validate_assignment=False,
19
+ str_strip_whitespace=True,
20
+ )
21
+
22
+
23
+ class BankAccount(SDKModel):
24
+ """
25
+ Bank account record returned by GET /rest/bankAccounts.
26
+
27
+ Note:
28
+ - OpenAPI lists `iBan` as required, but some examples omit it.
29
+ We keep it optional to avoid brittle SDK behavior.
30
+ """
31
+
32
+ id: int = Field(..., description="System-registered bank account ID.")
33
+ tin: str = Field(..., description="Taxpayer identification number (TIN) of the account owner.")
34
+
35
+ bank_account_no: str = Field(..., alias="bankAccountNo", description="Bank account number.")
36
+ bank_account_name: str = Field(..., alias="bankAccountName", description="Bank account name.")
37
+
38
+ bank_id: int = Field(..., alias="bankId", description="Bank ID.")
39
+ bank_name: str = Field(..., alias="bankName", description="Bank name.")
40
+
41
+ iban: str = Field(
42
+ alias="iBan",
43
+ description="International Bank Account Number (IBAN) / international account numbering.",
44
+ examples=["100100015121212121111"],
45
+ )
46
+
47
+ # If the API sometimes returns a nested blob like `data: {}`, we keep it explicitly.
48
+ data: dict[str, Any] | None = Field(default=None, description="Optional extra data payload.")
@@ -0,0 +1,30 @@
1
+ from ..resource import BaseResource, HeaderTypes, _build_headers, _ensure_http_success
2
+ from .schema import ReadInfoResponse
3
+
4
+
5
+ class InfoResource(BaseResource):
6
+ @property
7
+ def _path(self) -> str:
8
+ return "/rest/info"
9
+
10
+ def read(self, *, headers: HeaderTypes | None = None) -> ReadInfoResponse:
11
+ result = self._sync.send(
12
+ "GET",
13
+ self._path,
14
+ headers=_build_headers(self._headers, headers),
15
+ )
16
+
17
+ _ensure_http_success(result.response)
18
+
19
+ return ReadInfoResponse.model_validate(result.response.json())
20
+
21
+ async def aread(self, *, headers: HeaderTypes | None = None) -> ReadInfoResponse:
22
+ result = await self._async.send(
23
+ "GET",
24
+ self._path,
25
+ headers=_build_headers(self._headers, headers),
26
+ )
27
+
28
+ _ensure_http_success(result.response)
29
+
30
+ return ReadInfoResponse.model_validate(result.response.json())
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class AppInfo(BaseModel):
7
+ """
8
+ PosAPI-н ерөнхий мэдээлэл.
9
+ """
10
+
11
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
12
+
13
+ application_dir: str = Field(alias="applicationDir", description="Файл байршиж буй хавтас")
14
+ current_dir: str = Field(alias="currentDir", description="Файл байршиж буй хавтас")
15
+ database: str = Field(description="Баазын driver /Qt5 тохиргоогоор/")
16
+ database_host: str = Field(
17
+ alias="database-host", description="Баазын хаяг /sqlite бол файлын зам/"
18
+ )
19
+ work_dir: str = Field(alias="workDir", description="Ажиллаж буй хавтас")
20
+
21
+
22
+ class RestInfoCustomer(BaseModel):
23
+ """
24
+ Үйлчлүүлэгч ААН.
25
+ """
26
+
27
+ name: str = Field(description="ААН-нэр")
28
+ tin: str = Field(description="ААН-н ТТД")
29
+ vat_payer: bool = Field(
30
+ alias="vatPayer",
31
+ description="НӨАТ суутган төлөгч мөн эсэх\ntrue: НӨАТ суутган төлөгч мөн\nfalse: НӨАТ суутган төлөгч биш",
32
+ )
33
+
34
+
35
+ class Merchant(BaseModel):
36
+ """
37
+ PosAPI-д бүртгэлтэй ААН.
38
+ """
39
+
40
+ name: str = Field(description="ААН-нэр")
41
+ tin: str = Field(description="ААН-н ТТД")
42
+ customers: list[RestInfoCustomer] = Field(description="Үйлчлүүлэгч ААН-н жагсаалт")
43
+
44
+
45
+ class ReadInfoResponse(BaseModel):
46
+ """
47
+ GET /rest/info (200 OK) response schema.
48
+ """
49
+
50
+ operator_name: str = Field(alias="operatorName", description="Оператор байгууллагын нэр")
51
+ operator_tin: str = Field(alias="operatorTIN", description="Оператор байгууллагын ТТД")
52
+ pos_id: float = Field(alias="posId", description="PosAPI-н систем дэх бүртгэлийн Id")
53
+ pos_no: str = Field(alias="posNo", description="PosAPI-н систем дэх бүртгэлийн дугаар")
54
+ last_sent_date: str = Field(
55
+ alias="lastSentDate", description="Баримт илгээсэн огноо /Сүүлийн байдлаар/"
56
+ )
57
+ left_lotteries: int = Field(alias="leftLotteries", description="Нийт үлдсэн сугалаа")
58
+ app_info: AppInfo = Field(alias="appInfo", description="PosAPI-н ерөнхий мэдээлэл")
59
+ merchants: list[Merchant] = Field(description="PosAPI-д бүртгэлтэй ААН-н жагсаалт")
60
+
61
+ model_config = ConfigDict(populate_by_name=True)
@@ -0,0 +1,37 @@
1
+ from .schema import (
2
+ BarCodeType,
3
+ CreateReceiptRequest,
4
+ CreateReceiptResponse,
5
+ DeleteReceiptRequest,
6
+ DeleteReceiptResponse,
7
+ Item,
8
+ Payment,
9
+ PaymentCardData,
10
+ PaymentCode,
11
+ PaymentStatus,
12
+ Receipt,
13
+ ReceiptCreateStatus,
14
+ ReceiptItemData,
15
+ ReceiptItemResponse,
16
+ ReceiptType,
17
+ TaxType,
18
+ )
19
+
20
+ __all__ = [
21
+ "CreateReceiptRequest",
22
+ "CreateReceiptResponse",
23
+ "DeleteReceiptRequest",
24
+ "DeleteReceiptResponse",
25
+ "ReceiptItemResponse",
26
+ "Item",
27
+ "Payment",
28
+ "PaymentCardData",
29
+ "PaymentCode",
30
+ "PaymentStatus",
31
+ "Receipt",
32
+ "ReceiptCreateStatus",
33
+ "ReceiptItemData",
34
+ "BarCodeType",
35
+ "ReceiptType",
36
+ "TaxType",
37
+ ]
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..resource import (
6
+ BaseResource,
7
+ HeaderTypes,
8
+ _build_headers,
9
+ _ensure_http_success,
10
+ _validate_payload,
11
+ )
12
+ from .schema import (
13
+ CreateReceiptRequest,
14
+ CreateReceiptResponse,
15
+ DeleteReceiptRequest,
16
+ DeleteReceiptResponse,
17
+ )
18
+
19
+ _DEFAULT_HEADERS = {"Accept": "application/json"}
20
+
21
+
22
+ class ReceiptResource(BaseResource):
23
+ @property
24
+ def _path(self) -> str:
25
+ return "/rest/receipt"
26
+
27
+ def create(
28
+ self,
29
+ payload: CreateReceiptRequest | dict[str, Any],
30
+ *,
31
+ headers: HeaderTypes | None = None,
32
+ ) -> CreateReceiptResponse:
33
+ payload = _validate_payload(model=CreateReceiptRequest, payload=payload)
34
+
35
+ result = self._sync.send(
36
+ "POST",
37
+ self._path,
38
+ headers=_build_headers(self._headers, headers),
39
+ payload=payload.model_dump(mode="json", by_alias=True, exclude_none=True),
40
+ )
41
+
42
+ _ensure_http_success(result.response)
43
+
44
+ return CreateReceiptResponse.model_validate(result.response.json())
45
+
46
+ async def acreate(
47
+ self,
48
+ payload: CreateReceiptRequest | dict[str, Any],
49
+ *,
50
+ headers: HeaderTypes | None = None,
51
+ ) -> CreateReceiptResponse:
52
+ payload = _validate_payload(model=CreateReceiptRequest, payload=payload)
53
+
54
+ result = await self._async.send(
55
+ "POST",
56
+ self._path,
57
+ headers=_build_headers(self._headers, headers),
58
+ payload=payload.model_dump(mode="json", by_alias=True, exclude_none=True),
59
+ )
60
+
61
+ _ensure_http_success(result.response)
62
+
63
+ return CreateReceiptResponse.model_validate(result.response.json())
64
+
65
+ def delete(
66
+ self, payload: DeleteReceiptRequest | dict[str, Any], *, headers: HeaderTypes | None = None
67
+ ) -> DeleteReceiptResponse:
68
+ payload = _validate_payload(model=DeleteReceiptRequest, payload=payload)
69
+
70
+ result = self._sync.send(
71
+ "POST",
72
+ self._path,
73
+ headers=_build_headers(self._headers, headers),
74
+ payload=payload.model_dump(mode="json", by_alias=True, exclude_none=True),
75
+ )
76
+
77
+ _ensure_http_success(result.response)
78
+
79
+ return DeleteReceiptResponse.model_validate(result.response.json())
80
+
81
+ async def adelete(
82
+ self, payload: DeleteReceiptRequest | dict[str, Any], *, headers: HeaderTypes | None = None
83
+ ) -> DeleteReceiptResponse:
84
+ payload = _validate_payload(model=DeleteReceiptRequest, payload=payload)
85
+
86
+ result = await self._async.send(
87
+ "POST",
88
+ self._path,
89
+ headers=_build_headers(self._headers, headers),
90
+ payload=payload.model_dump(mode="json", by_alias=True, exclude_none=True),
91
+ )
92
+
93
+ _ensure_http_success(result.response)
94
+
95
+ return DeleteReceiptResponse.model_validate(result.response.json())
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from decimal import Decimal
5
+ from enum import Enum
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, ConfigDict
9
+
10
+ # Json building block types
11
+ Number = Decimal | int | float
12
+
13
+
14
+ class ReceiptType(str, Enum):
15
+ B2C_RECEIPT = "B2C_RECEIPT"
16
+ B2B_RECEIPT = "B2B_RECEIPT"
17
+ B2C_INVOICE = "B2C_INVOICE"
18
+ B2B_INVOICE = "B2B_INVOICE"
19
+
20
+
21
+ class TaxType(str, Enum):
22
+ VAT_ABLE = "VAT_ABLE"
23
+ VAT_FREE = "VAT_FREE"
24
+ VAT_ZERO = "VAT_ZERO"
25
+ NOT_VAT = "NOT_VAT"
26
+
27
+
28
+ class BarCodeType(str, Enum):
29
+ UNDEFINED = "UNDEFINED"
30
+ GS1 = "GS1"
31
+ ISBN = "ISBN"
32
+
33
+
34
+ class PaymentCode(str, Enum):
35
+ CASH = "CASH"
36
+ PAYMENT_CARD = "PAYMENT_CARD"
37
+
38
+
39
+ class PaymentStatus(str, Enum):
40
+ PAID = "PAID"
41
+ PAY = "PAY"
42
+ REVERSED = "REVERSED"
43
+ ERROR = "ERROR"
44
+
45
+
46
+ class ReceiptCreateStatus(str, Enum):
47
+ SUCCESS = "SUCCESS"
48
+ ERROR = "ERROR"
49
+ PAYMENT = "PAYMENT"
50
+
51
+
52
+ class ReceiptItemData(BaseModel):
53
+ """
54
+ items[].data
55
+ Note: docs show lotNo + stockQR, but examples also show nested "data": {"stockQR":[...]} sometimes.
56
+ This model follows the schema: lotNo, stockQR directly under data.
57
+ """
58
+
59
+ model_config = ConfigDict(extra="ignore")
60
+
61
+ lotNo: str | None = None
62
+ stockQR: list[str] | None = None
63
+
64
+
65
+ class Item(BaseModel):
66
+ model_config = ConfigDict(extra="ignore")
67
+
68
+ name: str
69
+ barCode: str
70
+ measureUnit: str
71
+ qty: Number
72
+ unitPrice: Number
73
+ totalAmount: Number
74
+
75
+ barCodeType: BarCodeType | str | None = None
76
+ classificationCode: str | None = None
77
+ taxProductCode: str | None = None
78
+ totalVAT: Number | None = None
79
+ totalCityTax: Number | None = None
80
+ data: ReceiptItemData | None = None
81
+
82
+
83
+ class Receipt(BaseModel):
84
+ model_config = ConfigDict(extra="ignore")
85
+
86
+ totalAmount: Number
87
+ taxType: TaxType | str
88
+ merchantTin: str
89
+ items: list[Item]
90
+
91
+ totalVAT: Number | None = None
92
+ totalCityTax: Number | None = None
93
+ customerTin: str | None = None
94
+
95
+ bankAccountNo: str | None = None
96
+ iBan: str | None = None
97
+ invoiceId: str | None = None
98
+ data: dict[str, Any] | None = None
99
+
100
+
101
+ class PaymentCardData(BaseModel):
102
+ """
103
+ payments[].data when code=PAYMENT_CARD
104
+ """
105
+
106
+ model_config = ConfigDict(extra="ignore")
107
+
108
+ terminalID: str
109
+ rrn: str
110
+ maskedCardNumber: str
111
+ easy: bool
112
+ bankCode: str | None = None
113
+
114
+
115
+ class Payment(BaseModel):
116
+ model_config = ConfigDict(extra="ignore")
117
+
118
+ code: PaymentCode | str
119
+ status: PaymentStatus | str
120
+ paidAmount: Number
121
+
122
+ exchangeCode: str | None = None
123
+ data: PaymentCardData | None = None # Only when code is PAYMENT_CARD
124
+
125
+
126
+ class CreateReceiptRequest(BaseModel):
127
+ model_config = ConfigDict(extra="ignore")
128
+
129
+ branchNo: str
130
+ totalAmount: Number
131
+ merchantTin: str
132
+ posNo: str
133
+ type: ReceiptType | str
134
+ billIdSuffix: str
135
+ receipts: list[Receipt]
136
+
137
+ totalVAT: Number | None = None
138
+ totalCityTax: Number | None = None
139
+ districtCode: str | None = None
140
+ customerTin: str | None = None
141
+ consumerNo: str | None = None
142
+ inactiveId: str | None = None
143
+ invoiceId: str | None = None
144
+ reportMonth: str | None = None
145
+ data: dict[str, Any] | None = None
146
+
147
+ payments: list[Payment] | None = None
148
+
149
+
150
+ class ReceiptItemResponse(BaseModel):
151
+ model_config = ConfigDict(extra="ignore")
152
+
153
+ id: str
154
+ bankAccountId: int
155
+
156
+
157
+ class CreateReceiptResponse(BaseModel):
158
+ model_config = ConfigDict(extra="ignore")
159
+
160
+ id: str
161
+ posId: int
162
+ status: ReceiptCreateStatus | str
163
+ message: str
164
+ qrDate: str
165
+ lottery: str
166
+ date: datetime
167
+ easy: bool
168
+ receipts: list[ReceiptItemResponse]
169
+
170
+
171
+ class DeleteReceiptRequest(BaseModel):
172
+ model_config = ConfigDict(extra="ignore")
173
+
174
+ id: str
175
+ date: datetime
176
+
177
+
178
+ class DeleteReceiptResponse(BaseModel):
179
+ model_config = ConfigDict(extra="ignore")
180
+
181
+ # TODO
@@ -0,0 +1,66 @@
1
+ # src/ebarimt_pos_sdk/resources/receipt.py
2
+ from __future__ import annotations
3
+
4
+ from abc import abstractmethod
5
+ from typing import Any, TypeVar
6
+
7
+ import httpx
8
+ from pydantic import BaseModel
9
+
10
+ from ..errors import PosApiHttpError
11
+ from ..transport import AsyncTransport, HeaderTypes, SyncTransport
12
+
13
+ T = TypeVar("T", bound=BaseModel)
14
+
15
+
16
+ class BaseResource:
17
+ def __init__(
18
+ self,
19
+ *,
20
+ sync: SyncTransport,
21
+ async_: AsyncTransport,
22
+ headers: HeaderTypes | None = None,
23
+ ) -> None:
24
+ self._sync = sync
25
+ self._async = async_
26
+ self._headers = headers
27
+
28
+ @property
29
+ @abstractmethod
30
+ def _path(self) -> str: ...
31
+
32
+
33
+ def _ensure_http_success(response: httpx.Response) -> httpx.Response:
34
+ try:
35
+ response.raise_for_status()
36
+ except httpx.HTTPStatusError as exc:
37
+ raise PosApiHttpError(
38
+ f"HTTP {exc.response.status_code}",
39
+ request=exc.request,
40
+ response=exc.response,
41
+ ) from exc
42
+ return response
43
+
44
+
45
+ def _validate_payload(model: type[T], payload: T | dict[str, Any]) -> T:
46
+ if isinstance(payload, model):
47
+ return payload
48
+ return model.model_validate(payload)
49
+
50
+
51
+ def _build_headers(*headers: HeaderTypes | None) -> httpx.Headers:
52
+ """Merges headers and returns one header.
53
+
54
+ Note:
55
+ Highest priority header should be the last.
56
+
57
+ Returns:
58
+ httpx.Headers: Merged header.
59
+ """
60
+ out = httpx.Headers()
61
+ for header in headers:
62
+ out.update(header)
63
+ return out
64
+
65
+
66
+ __all__ = ["_ensure_http_success", "_validate_payload", "_build_headers"]
@@ -0,0 +1,30 @@
1
+
2
+ from ..resource import BaseResource, HeaderTypes, _build_headers, _ensure_http_success
3
+
4
+
5
+ class SendDataResource(BaseResource):
6
+ @property
7
+ def _path(self) -> str:
8
+ return "/rest/sendData"
9
+
10
+ def send(self, headers: HeaderTypes | None = None) -> None:
11
+ result = self._sync.send(
12
+ "GET",
13
+ self._path,
14
+ headers=_build_headers(self._headers, headers),
15
+ )
16
+
17
+ _ensure_http_success(result.response)
18
+
19
+ return None
20
+
21
+ async def asend(self, headers: HeaderTypes | None = None) -> None:
22
+ result = await self._async.send(
23
+ "GET",
24
+ self._path,
25
+ headers=_build_headers(self._headers, headers),
26
+ )
27
+
28
+ _ensure_http_success(result.response)
29
+
30
+ return None
File without changes
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .transport import HeaderTypes
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class PosApiSettings:
10
+ """
11
+ SDK runtime settings.
12
+
13
+ Notes:
14
+ POS API usually runs on localhost or private network.
15
+ `base_url` may be http://localhost:7080 or an internal hostname behind VPN.
16
+ """
17
+
18
+ base_url: str = "http://localhost:7080"
19
+ timeout_s: float = 10.0
20
+ verify_tls: bool = True
21
+
22
+ # auth is intentionally flexible until spec is clearer:
23
+ # e.g. {"Authorization": "Bearer ..."} or {"X-API-KEY": "..."}
24
+ default_headers: HeaderTypes | None = None
25
+
26
+ def normalized_base_url(self) -> str:
27
+ """Normalizes base_url for clients to use.
28
+
29
+ Returns:
30
+ str: Normalized base_url.
31
+ """
32
+ return self.base_url.strip().rstrip("/")
@@ -0,0 +1,20 @@
1
+ """
2
+ This is the transport layer. It concerns with:
3
+ * send request
4
+ * handle network errors
5
+ * handle non-2xx HTTP errors
6
+ * decode JSON (or 204/empty)
7
+ * produce structured context (request/response + metadata)
8
+ """
9
+
10
+ from .async_transport import AsyncTransport
11
+ from .http import HeaderTypes, HttpRequestResponse, QueryParamTypes
12
+ from .sync_transport import SyncTransport
13
+
14
+ __all__ = [
15
+ "AsyncTransport",
16
+ "SyncTransport",
17
+ "HttpRequestResponse",
18
+ "HeaderTypes",
19
+ "QueryParamTypes",
20
+ ]
@@ -0,0 +1,48 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from .http import (
6
+ HeaderTypes,
7
+ HttpMethod,
8
+ HttpRequestResponse,
9
+ QueryParamTypes,
10
+ build_transport_error,
11
+ )
12
+
13
+
14
+ class AsyncTransport:
15
+ """Async request/response transport layer."""
16
+
17
+ def __init__(self, client: httpx.AsyncClient) -> None:
18
+ self._client = client
19
+
20
+ async def send(
21
+ self,
22
+ method: HttpMethod,
23
+ url: httpx.URL | str,
24
+ *,
25
+ params: QueryParamTypes | None = None,
26
+ headers: HeaderTypes | None = None,
27
+ payload: dict[str, Any] | None = None,
28
+ **kwargs: Any,
29
+ ) -> HttpRequestResponse:
30
+ # build http request context
31
+ request = self._client.build_request(
32
+ method=method,
33
+ url=url,
34
+ params=params,
35
+ headers=headers,
36
+ json=payload,
37
+ **kwargs,
38
+ )
39
+
40
+ try:
41
+ response = await self._client.send(request)
42
+ except httpx.HTTPError as exc:
43
+ raise build_transport_error(request, exc) from exc
44
+
45
+ return HttpRequestResponse(
46
+ request=request,
47
+ response=response,
48
+ )
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from dataclasses import dataclass
5
+ from typing import Literal, TypeAlias
6
+
7
+ import httpx
8
+
9
+ from ..errors import (
10
+ PosApiTransportError,
11
+ )
12
+
13
+ HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class HttpRequestResponse:
18
+ request: httpx.Request
19
+ response: httpx.Response
20
+
21
+ def as_tuple(self) -> tuple[httpx.Request, httpx.Response]:
22
+ return (self.request, self.response)
23
+
24
+
25
+ def build_transport_error(
26
+ request: httpx.Request,
27
+ exc: httpx.HTTPError,
28
+ ) -> PosApiTransportError:
29
+ """Build Pos Api Transport layer error."""
30
+ return PosApiTransportError(
31
+ f"Transport error for {request.method} {request.url}: {exc}",
32
+ request=request,
33
+ )
34
+
35
+
36
+ PrimitiveData = str | int | float | bool | None
37
+
38
+ QueryParamTypes: TypeAlias = (
39
+ httpx.QueryParams
40
+ | Mapping[str, PrimitiveData | Sequence[PrimitiveData]]
41
+ | list[tuple[str, PrimitiveData]]
42
+ | tuple[tuple[str, PrimitiveData], ...]
43
+ | str
44
+ )
45
+ HeaderTypes: TypeAlias = (
46
+ httpx.Headers
47
+ | Mapping[str, str]
48
+ | Mapping[bytes, bytes]
49
+ | Sequence[tuple[str, str]]
50
+ | Sequence[tuple[bytes, bytes]]
51
+ )
@@ -0,0 +1,39 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ from .http import HttpMethod, HttpRequestResponse, QueryParamTypes, build_transport_error
6
+
7
+
8
+ class SyncTransport:
9
+ def __init__(self, client: httpx.Client) -> None:
10
+ self._client = client
11
+
12
+ def send(
13
+ self,
14
+ method: HttpMethod,
15
+ url: httpx.URL | str,
16
+ *,
17
+ params: QueryParamTypes | None = None,
18
+ headers: httpx.Headers | None = None,
19
+ payload: dict[str, Any] | None = None,
20
+ **kwargs: Any,
21
+ ) -> HttpRequestResponse:
22
+ request = self._client.build_request(
23
+ method=method,
24
+ url=url,
25
+ params=params,
26
+ headers=headers,
27
+ json=payload,
28
+ **kwargs,
29
+ )
30
+
31
+ try:
32
+ response = self._client.send(request)
33
+ except httpx.HTTPError as exc:
34
+ raise build_transport_error(request, exc) from exc
35
+
36
+ return HttpRequestResponse(
37
+ request=request,
38
+ response=response,
39
+ )
File without changes
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.3
2
+ Name: ebarimt-pos-sdk
3
+ Version: 0.0.1b2
4
+ Summary: Python SDK for Ebarimt POS API 3.0
5
+ Author: SenergyTech
6
+ License: MIT
7
+ Requires-Dist: httpx>=0.27.0
8
+ Requires-Dist: pydantic>=2.7.0
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+
12
+ # ebarimt-pos-sdk
13
+
14
+ [![codecov](https://codecov.io/gh/Amraa1/ebarimt-pos-sdk/graph/badge.svg?token=EZ18HFDG46)](https://codecov.io/gh/Amraa1/ebarimt-pos-sdk)
15
+
16
+ Modern async-first Python SDK for Ebarimt Pos API 3.0.
17
+
18
+ > Ebarimt Pos API 3.0 [documentation](https://developer.itc.gov.mn/docs/ebarimt-api/inbishdm2zj3x-pos-api-3-0-sistemijn-api-holbolt-zaavruud)
19
+
20
+ ## Development setup
21
+
22
+ ```bash
23
+ uv sync --dev
24
+ uv run pytest
25
+ ```
26
+
27
+ # PosAPI тохируулах:
28
+
29
+ PosAPI нь суусны дараа анхны байдлаар тохируулах шаардлагатай. “posapi.ini”
30
+ файлд тухайн PosAPI-н үндсэн тохиргоо байрлах ба “P101.poi, P102.poi” файлуудад
31
+ ажиллагааны тохиргоо байрлах ба нууцлагдсан байна.
32
+
33
+ ### Үндсэн тохиргооны тайлбар /posapi.ini файл/
34
+
35
+ | Нэр | Тайлбар |
36
+ | ---------------- | -------------------------------------------------------------- |
37
+ | authUrl | |
38
+ | authRealm | Тухайн PosAPI-н нэгдсэн нэвтрэлттэй холбогдох тохиргоо |
39
+ | authClientId | Өөрчлөх шаардлагагүй. |
40
+ | authClientSecret | |
41
+ | ebarimtUrl | Ebarimt системтэй холбогдох хаяг Өөрчлөх шаардлагагүй |
42
+ | db | Өгөгдлийн сангийн driver |
43
+ | dbHost | Өгөгдлийн сангийн хаяг Хэрэв QSQLITE бол файлын зам байна |
44
+ | dbPort | Өгөгдлийн сангийн port Хэрэв QSQLITE бол бөглөхгүй |
45
+ | dbUser | Өгөгдлийн сангийн хэрэглэгчийн нэр Хэрэв QSQLITE бол бөглөхгүй |
46
+ | dbPass | Өгөгдлийн сангийн нууц үг хэрэв QSQLITE бол бөглөхгүй |
47
+ | dbName | Өгөгдлийн сангийн баазын нэр Хэрэв QSQLITE бол бөглөхгүй |
48
+ | dbOptions | Өгөгдлийн сангийн нэмэлт тохиргоо Хэрэв QSQLITE бол бөглөхгүй |
49
+ | workDir | PosAPI-н ажиллагааны хавтас |
50
+ | webServiceHost | PosAPI-н ажиллах сүлжээний IP address |
51
+ | webServicePort | PosAPI-н ажиллах сүлжээний port |
52
+
53
+ **WorkDir** хавтсанд ажиллагааны тохиргоо байрлах ба уг тохиргооны файлуудын агуулга
54
+ нь тогтмол өөрчлөгдөж байх тул PosAPI ажиллуулж буй хэрэглэгч нь унших, бичих
55
+ эрхтэй байхыг анхаарана уу. Мөн уг хавтсыг ямар ч нөхцөлд **FREEZE хийх ёсгүй**
56
+ гэдгийг анхаарна уу.
57
+
58
+ ### PosAPI-н дэмжиж ажиллах өгөгдлийн сангууд ба driver-ууд
59
+
60
+ | Нэр | Тайлбар |
61
+ | ------- | ----------------------------- |
62
+ | QMYSQL | MySQL эсвэл MariaDB |
63
+ | QPSQL | PostgreSQL |
64
+ | QODBC | ODBC for Microsoft SQL Server |
65
+ | QSQLITE | SQLite version 3 |
66
+
67
+ PosAPI нь ачааллах үедээ өгөгдлийн сангийн table-г автоматаар өөрөө үүсгэдэг тул
68
+ тухайн хэрэглэгч нь table үүсгэх эрх бүхий хэрэглэгч байх шаардлагатайг анхаарна уу
@@ -0,0 +1,24 @@
1
+ ebarimt_pos_sdk/__init__.py,sha256=sQPlSH_IE69ZnZiup0Uvks1vcz_gImCD_R4mGE9Nwc0,828
2
+ ebarimt_pos_sdk/client.py,sha256=aua7ruFgS1DoUzNAonpR47O_lzAh9PrTU8mlSdCff24,2930
3
+ ebarimt_pos_sdk/errors.py,sha256=faDkMH9yr9KvjrRgS0pyUnvO8l3vEpKCLr3sHyDf8fQ,1565
4
+ ebarimt_pos_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ ebarimt_pos_sdk/resources/__init__.py,sha256=YPnEmw-RhCK7zFFt59tqjjzNulR2t08SPVv4D9F-qZ4,284
6
+ ebarimt_pos_sdk/resources/bank_accounts/bank_accounts.py,sha256=k5PkBdyLlfXZaVqcYpWvaE0r-0QtMPM2X-HyeRNscg0,1397
7
+ ebarimt_pos_sdk/resources/bank_accounts/schema.py,sha256=qLjGxMRDbahglWITA_lPJz3saTdj_Eev66aYQkucmZg,1621
8
+ ebarimt_pos_sdk/resources/info/info.py,sha256=BESBm7W5JL_ihW-RL7sSNyVPjJWwh2jHl25jgoRNW2Q,936
9
+ ebarimt_pos_sdk/resources/info/schema.py,sha256=Sza96EyQ9oocyFr5eDOS7t7sTTT0HzBA3ocDLq1Fwfs,2628
10
+ ebarimt_pos_sdk/resources/receipt/__init__.py,sha256=y3G2eCUIs2AhI4pDVnsc9fGUpt-9-y6N9msSyGQqI7E,705
11
+ ebarimt_pos_sdk/resources/receipt/receipt.py,sha256=gTHPTauQYXlWf5eeOLo4IgDKCioTe1sOkNvN7xZKknQ,2922
12
+ ebarimt_pos_sdk/resources/receipt/schema.py,sha256=QudB87TAToa33h1iZd0GdVarOW_Dwv0-17U8Ti8jUYc,3883
13
+ ebarimt_pos_sdk/resources/resource.py,sha256=Ia4rZXTutLXARx3mommmmVB98FDKYuJeKFE4LVeqSNI,1593
14
+ ebarimt_pos_sdk/resources/send_data/send_data.py,sha256=cvLl2jeLEp0kDfSw59gbZwEecKciDkejtAh7WJgULLs,776
15
+ ebarimt_pos_sdk/resources/third_party/third_party.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ ebarimt_pos_sdk/settings.py,sha256=tMLbUo_Pfqr1va5NVPor5-fgs0onn1Ndkduf3WlLCdU,841
17
+ ebarimt_pos_sdk/transport/__init__.py,sha256=QD_WtnjPd2PQAV3ycnbItEuOyTMExIWdNj-wWgEQwjE,493
18
+ ebarimt_pos_sdk/transport/async_transport.py,sha256=2tURA1JgBmcM8ofkXfWLWIFcRz585v83XeWxziTkGK0,1149
19
+ ebarimt_pos_sdk/transport/http.py,sha256=Yj5lNKWHMdGckhPwH5OnJ8p_BA3WgL5s8SWCQYDJdug,1237
20
+ ebarimt_pos_sdk/transport/sync_transport.py,sha256=z5RPt3NU5UVlwgmupLNeQE6TlYgve2XXbFVr7Fuorbw,1007
21
+ ebarimt_pos_sdk/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ ebarimt_pos_sdk-0.0.1b2.dist-info/WHEEL,sha256=5DEXXimM34_d4Gx1AuF9ysMr1_maoEtGKjaILM3s4w4,80
23
+ ebarimt_pos_sdk-0.0.1b2.dist-info/METADATA,sha256=m-XFrBdpTZ2j31Xt74OWYvmtWTZkHvzM9DT8khSr8Ks,4164
24
+ ebarimt_pos_sdk-0.0.1b2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.29
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any