ebarimt-pos-sdk 0.0.1b2__tar.gz → 0.1.0__tar.gz

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.
Files changed (25) hide show
  1. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/PKG-INFO +1 -1
  2. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/pyproject.toml +10 -18
  3. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/__init__.py +14 -0
  4. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/errors.py +46 -8
  5. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/resources/bank_accounts/bank_accounts.py +7 -7
  6. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/resources/info/info.py +7 -7
  7. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/resources/receipt/receipt.py +17 -25
  8. ebarimt_pos_sdk-0.1.0/src/ebarimt_pos_sdk/resources/resource.py +91 -0
  9. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/resources/send_data/send_data.py +5 -6
  10. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/settings.py +1 -0
  11. ebarimt_pos_sdk-0.0.1b2/src/ebarimt_pos_sdk/resources/resource.py +0 -66
  12. ebarimt_pos_sdk-0.0.1b2/src/ebarimt_pos_sdk/resources/third_party/third_party.py +0 -0
  13. ebarimt_pos_sdk-0.0.1b2/src/ebarimt_pos_sdk/utils.py +0 -0
  14. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/README.md +0 -0
  15. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/client.py +0 -0
  16. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/py.typed +0 -0
  17. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/resources/__init__.py +0 -0
  18. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/resources/bank_accounts/schema.py +0 -0
  19. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/resources/info/schema.py +0 -0
  20. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/resources/receipt/__init__.py +0 -0
  21. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/resources/receipt/schema.py +0 -0
  22. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/transport/__init__.py +0 -0
  23. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/transport/async_transport.py +0 -0
  24. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/transport/http.py +0 -0
  25. {ebarimt_pos_sdk-0.0.1b2 → ebarimt_pos_sdk-0.1.0}/src/ebarimt_pos_sdk/transport/sync_transport.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ebarimt-pos-sdk
3
- Version: 0.0.1b2
3
+ Version: 0.1.0
4
4
  Summary: Python SDK for Ebarimt POS API 3.0
5
5
  Author: SenergyTech
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ebarimt-pos-sdk"
3
- version = "0.0.1b2"
3
+ version = "0.1.0"
4
4
  description = "Python SDK for Ebarimt POS API 3.0"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -11,12 +11,12 @@ dependencies = ["httpx>=0.27.0", "pydantic>=2.7.0"]
11
11
 
12
12
  [dependency-groups]
13
13
  dev = [
14
- "pytest>=9.0.2",
15
- "pytest-asyncio>=1.3.0",
16
- "respx>=0.22.0",
17
- "ruff>=0.5.0",
18
- "mypy>=1.10.0",
19
- "pytest-cov>=7.0.0",
14
+ "pytest>=9.0.2",
15
+ "pytest-asyncio>=1.3.0",
16
+ "respx>=0.22.0",
17
+ "ruff>=0.5.0",
18
+ "mypy>=1.10.0",
19
+ "pytest-cov>=7.0.0",
20
20
  ]
21
21
 
22
22
  [build-system]
@@ -26,22 +26,14 @@ build-backend = "uv_build"
26
26
  [tool.pytest.ini_options]
27
27
  asyncio_mode = "auto"
28
28
  testpaths = ["tests"]
29
+ markers = ["integration: tests that require a real PosAPI server"]
29
30
 
30
31
  [tool.ruff]
31
32
  line-length = 100
32
33
  target-version = "py310"
34
+ exclude = ["tests"]
35
+ src = ["src"]
33
36
 
34
37
  [tool.ruff.lint]
35
38
  select = ["E", "F", "I", "B", "UP"]
36
39
  ignore = ["E501"]
37
-
38
- [tool.mypy]
39
- python_version = "3.10"
40
- warn_return_any = true
41
- warn_unused_ignores = true
42
- no_implicit_optional = true
43
- disallow_untyped_defs = true
44
- check_untyped_defs = true
45
- mypy_path = ["src"]
46
- files = ["src"]
47
- exclude = '^tests/'
@@ -1,4 +1,12 @@
1
1
  from .client import PosApiClient
2
+ from .errors import (
3
+ PosApiBusinessError,
4
+ PosApiDecodeError,
5
+ PosApiError,
6
+ PosApiHttpError,
7
+ PosApiTransportError,
8
+ PosApiValidationError,
9
+ )
2
10
  from .resources.receipt import (
3
11
  BarCodeType,
4
12
  CreateReceiptRequest,
@@ -38,4 +46,10 @@ __all__ = [
38
46
  "ReceiptItemResponse",
39
47
  "ReceiptType",
40
48
  "TaxType",
49
+ "PosApiBusinessError",
50
+ "PosApiDecodeError",
51
+ "PosApiError",
52
+ "PosApiHttpError",
53
+ "PosApiTransportError",
54
+ "PosApiValidationError",
41
55
  ]
@@ -2,7 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import Literal
6
+
5
7
  import httpx
8
+ from pydantic import ValidationError
6
9
 
7
10
 
8
11
  class PosApiError(Exception):
@@ -14,10 +17,16 @@ class PosApiError(Exception):
14
17
  *,
15
18
  request: httpx.Request | None = None,
16
19
  response: httpx.Response | None = None,
20
+ cause: Exception | None = None,
17
21
  ) -> None:
18
22
  super().__init__(message)
23
+ self.message = message
19
24
  self.request = request
20
25
  self.response = response
26
+ self.cause = cause
27
+
28
+ def __str__(self) -> str:
29
+ return self.message
21
30
 
22
31
 
23
32
  class PosApiTransportError(PosApiError):
@@ -31,18 +40,47 @@ class PosApiDecodeError(PosApiError):
31
40
  class PosApiValidationError(PosApiError):
32
41
  """Pydantic validation errors (request or response)."""
33
42
 
34
-
35
- class PosApiHttpError(PosApiError):
36
- """Non-2xx response from server."""
37
-
38
43
  def __init__(
39
44
  self,
40
- message: str,
41
45
  *,
42
- request: httpx.Request,
43
- response: httpx.Response,
46
+ stage: Literal["request", "response"],
47
+ model: type | str,
48
+ validation_error: ValidationError,
49
+ request: httpx.Request | None = None,
50
+ response: httpx.Response | None = None,
44
51
  ) -> None:
45
- super().__init__(message, request=request, response=response)
52
+ self.stage = stage
53
+ self.model = model if isinstance(model, str) else model.__name__
54
+ self.validation_error = validation_error
55
+
56
+ message = f"Validation failed during {stage} for model '{self.model}'"
57
+
58
+ super().__init__(
59
+ message,
60
+ request=request,
61
+ response=response,
62
+ cause=validation_error,
63
+ )
64
+
65
+ @property
66
+ def errors(self) -> list:
67
+ """Return Pydantic-style validation errors."""
68
+ return self.validation_error.errors()
69
+
70
+ def __str__(self) -> str:
71
+ lines = [self.message]
72
+
73
+ for err in self.errors:
74
+ loc = ".".join(str(x) for x in err.get("loc", []))
75
+ msg = err.get("msg", "")
76
+ typ = err.get("type", "")
77
+ lines.append(f" - {loc}: {msg} ({typ})")
78
+
79
+ return "\n".join(lines)
80
+
81
+
82
+ class PosApiHttpError(PosApiError):
83
+ """Non-2xx response from server."""
46
84
 
47
85
 
48
86
  class PosApiBusinessError(PosApiError):
@@ -1,6 +1,6 @@
1
1
  import httpx
2
2
 
3
- from ..resource import BaseResource, HeaderTypes, _build_headers, _ensure_http_success
3
+ from ..resource import BaseResource, HeaderTypes
4
4
  from .schema import BankAccount
5
5
 
6
6
 
@@ -19,14 +19,14 @@ class BankAccountsResource(BaseResource):
19
19
  "GET",
20
20
  self._path,
21
21
  params=httpx.QueryParams({"tin": tin}),
22
- headers=_build_headers(self._headers, headers),
22
+ headers=self._build_headers(self._headers, headers),
23
23
  )
24
24
 
25
- success_response = _ensure_http_success(result.response)
25
+ response = self._ensure_http_success(result.response)
26
26
 
27
27
  output: list[BankAccount] = []
28
28
 
29
- for data in success_response.json():
29
+ for data in self._decode_json(response):
30
30
  output.append(BankAccount.model_validate(data))
31
31
 
32
32
  return output
@@ -41,14 +41,14 @@ class BankAccountsResource(BaseResource):
41
41
  "GET",
42
42
  self._path,
43
43
  params=httpx.QueryParams({"tin": tin}),
44
- headers=_build_headers(self._headers, headers),
44
+ headers=self._build_headers(self._headers, headers),
45
45
  )
46
46
 
47
- success_response = _ensure_http_success(result.response)
47
+ response = self._ensure_http_success(result.response)
48
48
 
49
49
  output: list[BankAccount] = []
50
50
 
51
- for data in success_response.json():
51
+ for data in self._decode_json(response):
52
52
  output.append(BankAccount.model_validate(data))
53
53
 
54
54
  return output
@@ -1,4 +1,4 @@
1
- from ..resource import BaseResource, HeaderTypes, _build_headers, _ensure_http_success
1
+ from ..resource import BaseResource, HeaderTypes
2
2
  from .schema import ReadInfoResponse
3
3
 
4
4
 
@@ -11,20 +11,20 @@ class InfoResource(BaseResource):
11
11
  result = self._sync.send(
12
12
  "GET",
13
13
  self._path,
14
- headers=_build_headers(self._headers, headers),
14
+ headers=self._build_headers(self._headers, headers),
15
15
  )
16
16
 
17
- _ensure_http_success(result.response)
17
+ self._ensure_http_success(result.response)
18
18
 
19
- return ReadInfoResponse.model_validate(result.response.json())
19
+ return ReadInfoResponse.model_validate(self._decode_json(result.response))
20
20
 
21
21
  async def aread(self, *, headers: HeaderTypes | None = None) -> ReadInfoResponse:
22
22
  result = await self._async.send(
23
23
  "GET",
24
24
  self._path,
25
- headers=_build_headers(self._headers, headers),
25
+ headers=self._build_headers(self._headers, headers),
26
26
  )
27
27
 
28
- _ensure_http_success(result.response)
28
+ self._ensure_http_success(result.response)
29
29
 
30
- return ReadInfoResponse.model_validate(result.response.json())
30
+ return ReadInfoResponse.model_validate(self._decode_json(result.response))
@@ -2,13 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
- from ..resource import (
6
- BaseResource,
7
- HeaderTypes,
8
- _build_headers,
9
- _ensure_http_success,
10
- _validate_payload,
11
- )
5
+ from ..resource import BaseResource, HeaderTypes
12
6
  from .schema import (
13
7
  CreateReceiptRequest,
14
8
  CreateReceiptResponse,
@@ -16,8 +10,6 @@ from .schema import (
16
10
  DeleteReceiptResponse,
17
11
  )
18
12
 
19
- _DEFAULT_HEADERS = {"Accept": "application/json"}
20
-
21
13
 
22
14
  class ReceiptResource(BaseResource):
23
15
  @property
@@ -30,18 +22,18 @@ class ReceiptResource(BaseResource):
30
22
  *,
31
23
  headers: HeaderTypes | None = None,
32
24
  ) -> CreateReceiptResponse:
33
- payload = _validate_payload(model=CreateReceiptRequest, payload=payload)
25
+ payload = self._validate_payload(model=CreateReceiptRequest, payload=payload)
34
26
 
35
27
  result = self._sync.send(
36
28
  "POST",
37
29
  self._path,
38
- headers=_build_headers(self._headers, headers),
30
+ headers=self._build_headers(self._headers, headers),
39
31
  payload=payload.model_dump(mode="json", by_alias=True, exclude_none=True),
40
32
  )
41
33
 
42
- _ensure_http_success(result.response)
34
+ self._ensure_http_success(result.response)
43
35
 
44
- return CreateReceiptResponse.model_validate(result.response.json())
36
+ return CreateReceiptResponse.model_validate(self._decode_json(result.response))
45
37
 
46
38
  async def acreate(
47
39
  self,
@@ -49,47 +41,47 @@ class ReceiptResource(BaseResource):
49
41
  *,
50
42
  headers: HeaderTypes | None = None,
51
43
  ) -> CreateReceiptResponse:
52
- payload = _validate_payload(model=CreateReceiptRequest, payload=payload)
44
+ payload = self._validate_payload(model=CreateReceiptRequest, payload=payload)
53
45
 
54
46
  result = await self._async.send(
55
47
  "POST",
56
48
  self._path,
57
- headers=_build_headers(self._headers, headers),
49
+ headers=self._build_headers(self._headers, headers),
58
50
  payload=payload.model_dump(mode="json", by_alias=True, exclude_none=True),
59
51
  )
60
52
 
61
- _ensure_http_success(result.response)
53
+ self._ensure_http_success(result.response)
62
54
 
63
- return CreateReceiptResponse.model_validate(result.response.json())
55
+ return CreateReceiptResponse.model_validate(self._decode_json(result.response))
64
56
 
65
57
  def delete(
66
58
  self, payload: DeleteReceiptRequest | dict[str, Any], *, headers: HeaderTypes | None = None
67
59
  ) -> DeleteReceiptResponse:
68
- payload = _validate_payload(model=DeleteReceiptRequest, payload=payload)
60
+ payload = self._validate_payload(model=DeleteReceiptRequest, payload=payload)
69
61
 
70
62
  result = self._sync.send(
71
63
  "POST",
72
64
  self._path,
73
- headers=_build_headers(self._headers, headers),
65
+ headers=self._build_headers(self._headers, headers),
74
66
  payload=payload.model_dump(mode="json", by_alias=True, exclude_none=True),
75
67
  )
76
68
 
77
- _ensure_http_success(result.response)
69
+ self._ensure_http_success(result.response)
78
70
 
79
- return DeleteReceiptResponse.model_validate(result.response.json())
71
+ return DeleteReceiptResponse.model_validate(self._decode_json(result.response))
80
72
 
81
73
  async def adelete(
82
74
  self, payload: DeleteReceiptRequest | dict[str, Any], *, headers: HeaderTypes | None = None
83
75
  ) -> DeleteReceiptResponse:
84
- payload = _validate_payload(model=DeleteReceiptRequest, payload=payload)
76
+ payload = self._validate_payload(model=DeleteReceiptRequest, payload=payload)
85
77
 
86
78
  result = await self._async.send(
87
79
  "POST",
88
80
  self._path,
89
- headers=_build_headers(self._headers, headers),
81
+ headers=self._build_headers(self._headers, headers),
90
82
  payload=payload.model_dump(mode="json", by_alias=True, exclude_none=True),
91
83
  )
92
84
 
93
- _ensure_http_success(result.response)
85
+ self._ensure_http_success(result.response)
94
86
 
95
- return DeleteReceiptResponse.model_validate(result.response.json())
87
+ return DeleteReceiptResponse.model_validate(self._decode_json(result.response))
@@ -0,0 +1,91 @@
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, ValidationError
9
+
10
+ from ..errors import PosApiDecodeError, PosApiHttpError, PosApiValidationError
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
+ @staticmethod
33
+ def _decode_json(response: httpx.Response) -> Any:
34
+ try:
35
+ return response.json()
36
+ except Exception as exc:
37
+ raise PosApiDecodeError(
38
+ "Failed to decode JSON response",
39
+ response=response,
40
+ ) from exc
41
+
42
+ @staticmethod
43
+ def _ensure_http_success(response: httpx.Response) -> httpx.Response:
44
+ try:
45
+ return response.raise_for_status()
46
+ except httpx.HTTPStatusError as exc:
47
+ raise PosApiHttpError(
48
+ f"HTTP {exc.response.status_code}",
49
+ request=exc.request,
50
+ response=exc.response,
51
+ ) from exc
52
+
53
+ @staticmethod
54
+ def _validate_payload(model: type[T], payload: T | dict[str, Any]) -> T:
55
+ if isinstance(payload, model):
56
+ return payload
57
+ try:
58
+ return model.model_validate(payload)
59
+ except ValidationError as exc:
60
+ raise PosApiValidationError(
61
+ stage="request",
62
+ model=model,
63
+ validation_error=exc,
64
+ ) from exc
65
+
66
+ @staticmethod
67
+ def _validate_response(model: type[T], response: Any) -> T:
68
+ try:
69
+ return model.model_validate(response)
70
+ except ValidationError as exc:
71
+ raise PosApiValidationError(
72
+ stage="response",
73
+ model=model,
74
+ validation_error=exc,
75
+ ) from exc
76
+
77
+ @staticmethod
78
+ def _build_headers(*headers: HeaderTypes | None) -> httpx.Headers:
79
+ """Merges headers and returns one header.
80
+
81
+ Note:
82
+ Highest priority header should be the last.
83
+
84
+ Returns:
85
+ httpx.Headers: Merged header.
86
+ """
87
+ out = httpx.Headers()
88
+ for header in headers:
89
+ if header is not None:
90
+ out.update(header)
91
+ return out
@@ -1,5 +1,4 @@
1
-
2
- from ..resource import BaseResource, HeaderTypes, _build_headers, _ensure_http_success
1
+ from ..resource import BaseResource, HeaderTypes
3
2
 
4
3
 
5
4
  class SendDataResource(BaseResource):
@@ -11,10 +10,10 @@ class SendDataResource(BaseResource):
11
10
  result = self._sync.send(
12
11
  "GET",
13
12
  self._path,
14
- headers=_build_headers(self._headers, headers),
13
+ headers=self._build_headers(self._headers, headers),
15
14
  )
16
15
 
17
- _ensure_http_success(result.response)
16
+ self._ensure_http_success(result.response)
18
17
 
19
18
  return None
20
19
 
@@ -22,9 +21,9 @@ class SendDataResource(BaseResource):
22
21
  result = await self._async.send(
23
22
  "GET",
24
23
  self._path,
25
- headers=_build_headers(self._headers, headers),
24
+ headers=self._build_headers(self._headers, headers),
26
25
  )
27
26
 
28
- _ensure_http_success(result.response)
27
+ self._ensure_http_success(result.response)
29
28
 
30
29
  return None
@@ -23,6 +23,7 @@ class PosApiSettings:
23
23
  # e.g. {"Authorization": "Bearer ..."} or {"X-API-KEY": "..."}
24
24
  default_headers: HeaderTypes | None = None
25
25
 
26
+ @property
26
27
  def normalized_base_url(self) -> str:
27
28
  """Normalizes base_url for clients to use.
28
29
 
@@ -1,66 +0,0 @@
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"]
File without changes