moco-py 1.0.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 (152) hide show
  1. moco_py-1.0.0/.github/workflows/release.yml +46 -0
  2. moco_py-1.0.0/.gitignore +46 -0
  3. moco_py-1.0.0/.pre-commit-config.yaml +6 -0
  4. moco_py-1.0.0/CHANGELOG.md +7 -0
  5. moco_py-1.0.0/PKG-INFO +30 -0
  6. moco_py-1.0.0/README.md +17 -0
  7. moco_py-1.0.0/pyproject.toml +72 -0
  8. moco_py-1.0.0/src/moco_py/__init__.py +36 -0
  9. moco_py-1.0.0/src/moco_py/_constants.py +9 -0
  10. moco_py-1.0.0/src/moco_py/_pagination.py +160 -0
  11. moco_py-1.0.0/src/moco_py/_resource.py +157 -0
  12. moco_py-1.0.0/src/moco_py/_response.py +27 -0
  13. moco_py-1.0.0/src/moco_py/_transport.py +401 -0
  14. moco_py-1.0.0/src/moco_py/client.py +707 -0
  15. moco_py-1.0.0/src/moco_py/exceptions.py +125 -0
  16. moco_py-1.0.0/src/moco_py/py.typed +0 -0
  17. moco_py-1.0.0/src/moco_py/resources/__init__.py +0 -0
  18. moco_py-1.0.0/src/moco_py/resources/account_custom_properties.py +224 -0
  19. moco_py-1.0.0/src/moco_py/resources/account_web_hooks.py +107 -0
  20. moco_py-1.0.0/src/moco_py/resources/activities.py +349 -0
  21. moco_py-1.0.0/src/moco_py/resources/comments.py +185 -0
  22. moco_py-1.0.0/src/moco_py/resources/companies.py +329 -0
  23. moco_py-1.0.0/src/moco_py/resources/contacts.py +307 -0
  24. moco_py-1.0.0/src/moco_py/resources/deal_categories.py +104 -0
  25. moco_py-1.0.0/src/moco_py/resources/deals.py +273 -0
  26. moco_py-1.0.0/src/moco_py/resources/employments.py +158 -0
  27. moco_py-1.0.0/src/moco_py/resources/holidays.py +158 -0
  28. moco_py-1.0.0/src/moco_py/resources/invoice_bookkeeping_exports.py +84 -0
  29. moco_py-1.0.0/src/moco_py/resources/invoice_payments.py +198 -0
  30. moco_py-1.0.0/src/moco_py/resources/invoice_reminders.py +180 -0
  31. moco_py-1.0.0/src/moco_py/resources/invoices.py +498 -0
  32. moco_py-1.0.0/src/moco_py/resources/offers.py +383 -0
  33. moco_py-1.0.0/src/moco_py/resources/planning_entries.py +239 -0
  34. moco_py-1.0.0/src/moco_py/resources/presences.py +202 -0
  35. moco_py-1.0.0/src/moco_py/resources/profile.py +23 -0
  36. moco_py-1.0.0/src/moco_py/resources/project_contracts.py +158 -0
  37. moco_py-1.0.0/src/moco_py/resources/project_expenses.py +448 -0
  38. moco_py-1.0.0/src/moco_py/resources/project_groups.py +34 -0
  39. moco_py-1.0.0/src/moco_py/resources/project_payment_schedules.py +248 -0
  40. moco_py-1.0.0/src/moco_py/resources/project_recurring_expenses.py +294 -0
  41. moco_py-1.0.0/src/moco_py/resources/project_tasks.py +176 -0
  42. moco_py-1.0.0/src/moco_py/resources/projects.py +616 -0
  43. moco_py-1.0.0/src/moco_py/resources/purchase_categories.py +36 -0
  44. moco_py-1.0.0/src/moco_py/resources/purchase_drafts.py +40 -0
  45. moco_py-1.0.0/src/moco_py/resources/purchase_payments.py +178 -0
  46. moco_py-1.0.0/src/moco_py/resources/purchases.py +435 -0
  47. moco_py-1.0.0/src/moco_py/resources/receipts.py +218 -0
  48. moco_py-1.0.0/src/moco_py/resources/reports.py +199 -0
  49. moco_py-1.0.0/src/moco_py/resources/schedules.py +207 -0
  50. moco_py-1.0.0/src/moco_py/resources/tags.py +110 -0
  51. moco_py-1.0.0/src/moco_py/resources/units.py +64 -0
  52. moco_py-1.0.0/src/moco_py/resources/user_roles.py +23 -0
  53. moco_py-1.0.0/src/moco_py/resources/users.py +286 -0
  54. moco_py-1.0.0/src/moco_py/resources/vat_codes.py +62 -0
  55. moco_py-1.0.0/src/moco_py/resources/work_time_adjustments.py +174 -0
  56. moco_py-1.0.0/src/moco_py/types/__init__.py +0 -0
  57. moco_py-1.0.0/src/moco_py/types/_embedded.py +44 -0
  58. moco_py-1.0.0/src/moco_py/types/_enums.py +330 -0
  59. moco_py-1.0.0/src/moco_py/types/account_custom_properties.py +26 -0
  60. moco_py-1.0.0/src/moco_py/types/account_web_hooks.py +22 -0
  61. moco_py-1.0.0/src/moco_py/types/activities.py +64 -0
  62. moco_py-1.0.0/src/moco_py/types/comments.py +23 -0
  63. moco_py-1.0.0/src/moco_py/types/companies.py +83 -0
  64. moco_py-1.0.0/src/moco_py/types/contacts.py +41 -0
  65. moco_py-1.0.0/src/moco_py/types/deal_categories.py +17 -0
  66. moco_py-1.0.0/src/moco_py/types/deals.py +55 -0
  67. moco_py-1.0.0/src/moco_py/types/employments.py +31 -0
  68. moco_py-1.0.0/src/moco_py/types/holidays.py +23 -0
  69. moco_py-1.0.0/src/moco_py/types/invoice_bookkeeping_exports.py +25 -0
  70. moco_py-1.0.0/src/moco_py/types/invoice_payments.py +23 -0
  71. moco_py-1.0.0/src/moco_py/types/invoice_reminders.py +25 -0
  72. moco_py-1.0.0/src/moco_py/types/invoices.py +174 -0
  73. moco_py-1.0.0/src/moco_py/types/offers.py +115 -0
  74. moco_py-1.0.0/src/moco_py/types/planning_entries.py +60 -0
  75. moco_py-1.0.0/src/moco_py/types/presences.py +24 -0
  76. moco_py-1.0.0/src/moco_py/types/profile.py +30 -0
  77. moco_py-1.0.0/src/moco_py/types/project_contracts.py +22 -0
  78. moco_py-1.0.0/src/moco_py/types/project_expenses.py +84 -0
  79. moco_py-1.0.0/src/moco_py/types/project_groups.py +50 -0
  80. moco_py-1.0.0/src/moco_py/types/project_payment_schedules.py +30 -0
  81. moco_py-1.0.0/src/moco_py/types/project_recurring_expenses.py +44 -0
  82. moco_py-1.0.0/src/moco_py/types/project_tasks.py +24 -0
  83. moco_py-1.0.0/src/moco_py/types/projects.py +158 -0
  84. moco_py-1.0.0/src/moco_py/types/purchase_categories.py +18 -0
  85. moco_py-1.0.0/src/moco_py/types/purchase_drafts.py +28 -0
  86. moco_py-1.0.0/src/moco_py/types/purchase_payments.py +26 -0
  87. moco_py-1.0.0/src/moco_py/types/purchases.py +139 -0
  88. moco_py-1.0.0/src/moco_py/types/receipts.py +72 -0
  89. moco_py-1.0.0/src/moco_py/types/reports.py +112 -0
  90. moco_py-1.0.0/src/moco_py/types/schedules.py +43 -0
  91. moco_py-1.0.0/src/moco_py/types/tags.py +18 -0
  92. moco_py-1.0.0/src/moco_py/types/units.py +25 -0
  93. moco_py-1.0.0/src/moco_py/types/user_roles.py +35 -0
  94. moco_py-1.0.0/src/moco_py/types/users.py +75 -0
  95. moco_py-1.0.0/src/moco_py/types/vat_codes.py +31 -0
  96. moco_py-1.0.0/src/moco_py/types/work_time_adjustments.py +22 -0
  97. moco_py-1.0.0/tests/__init__.py +0 -0
  98. moco_py-1.0.0/tests/integration/__init__.py +0 -0
  99. moco_py-1.0.0/tests/integration/conftest.py +59 -0
  100. moco_py-1.0.0/tests/integration/test_activities.py +57 -0
  101. moco_py-1.0.0/tests/integration/test_comments.py +30 -0
  102. moco_py-1.0.0/tests/integration/test_companies.py +54 -0
  103. moco_py-1.0.0/tests/integration/test_contacts.py +39 -0
  104. moco_py-1.0.0/tests/integration/test_deals.py +50 -0
  105. moco_py-1.0.0/tests/integration/test_projects.py +64 -0
  106. moco_py-1.0.0/tests/integration/test_tags.py +34 -0
  107. moco_py-1.0.0/tests/integration/test_users.py +26 -0
  108. moco_py-1.0.0/tests/test_account_custom_properties.py +130 -0
  109. moco_py-1.0.0/tests/test_account_web_hooks.py +131 -0
  110. moco_py-1.0.0/tests/test_activities.py +203 -0
  111. moco_py-1.0.0/tests/test_client.py +76 -0
  112. moco_py-1.0.0/tests/test_comments.py +142 -0
  113. moco_py-1.0.0/tests/test_companies.py +181 -0
  114. moco_py-1.0.0/tests/test_contacts.py +140 -0
  115. moco_py-1.0.0/tests/test_deal_categories.py +97 -0
  116. moco_py-1.0.0/tests/test_deals.py +146 -0
  117. moco_py-1.0.0/tests/test_employments.py +116 -0
  118. moco_py-1.0.0/tests/test_exceptions.py +107 -0
  119. moco_py-1.0.0/tests/test_holidays.py +108 -0
  120. moco_py-1.0.0/tests/test_invoice_bookkeeping_exports.py +88 -0
  121. moco_py-1.0.0/tests/test_invoice_payments.py +141 -0
  122. moco_py-1.0.0/tests/test_invoice_reminders.py +115 -0
  123. moco_py-1.0.0/tests/test_invoices.py +277 -0
  124. moco_py-1.0.0/tests/test_offers.py +164 -0
  125. moco_py-1.0.0/tests/test_pagination.py +144 -0
  126. moco_py-1.0.0/tests/test_planning_entries.py +162 -0
  127. moco_py-1.0.0/tests/test_presences.py +117 -0
  128. moco_py-1.0.0/tests/test_profile.py +51 -0
  129. moco_py-1.0.0/tests/test_project_contracts.py +103 -0
  130. moco_py-1.0.0/tests/test_project_expenses.py +185 -0
  131. moco_py-1.0.0/tests/test_project_groups.py +83 -0
  132. moco_py-1.0.0/tests/test_project_payment_schedules.py +117 -0
  133. moco_py-1.0.0/tests/test_project_recurring_expenses.py +165 -0
  134. moco_py-1.0.0/tests/test_project_tasks.py +118 -0
  135. moco_py-1.0.0/tests/test_projects.py +303 -0
  136. moco_py-1.0.0/tests/test_purchase_categories.py +67 -0
  137. moco_py-1.0.0/tests/test_purchase_drafts.py +79 -0
  138. moco_py-1.0.0/tests/test_purchase_payments.py +108 -0
  139. moco_py-1.0.0/tests/test_purchases.py +201 -0
  140. moco_py-1.0.0/tests/test_receipts.py +153 -0
  141. moco_py-1.0.0/tests/test_reports.py +194 -0
  142. moco_py-1.0.0/tests/test_resource.py +89 -0
  143. moco_py-1.0.0/tests/test_response.py +46 -0
  144. moco_py-1.0.0/tests/test_schedules.py +136 -0
  145. moco_py-1.0.0/tests/test_tags.py +114 -0
  146. moco_py-1.0.0/tests/test_transport.py +202 -0
  147. moco_py-1.0.0/tests/test_units.py +108 -0
  148. moco_py-1.0.0/tests/test_user_roles.py +62 -0
  149. moco_py-1.0.0/tests/test_users.py +157 -0
  150. moco_py-1.0.0/tests/test_vat_codes.py +108 -0
  151. moco_py-1.0.0/tests/test_work_time_adjustments.py +119 -0
  152. moco_py-1.0.0/uv.lock +1329 -0
@@ -0,0 +1,46 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: write
9
+ id-token: write
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ if: "!startsWith(github.event.head_commit.message, 'chore(release):')"
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ with:
19
+ fetch-depth: 0
20
+ token: ${{ secrets.GITHUB_TOKEN }}
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v6
24
+
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v5
27
+ with:
28
+ python-version: "3.12"
29
+
30
+ - name: Install dependencies
31
+ run: uv sync
32
+
33
+ - name: Run semantic-release
34
+ env:
35
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36
+ run: |
37
+ git config user.name "github-actions[bot]"
38
+ git config user.email "github-actions[bot]@users.noreply.github.com"
39
+ uv run semantic-release version
40
+ uv run semantic-release publish
41
+
42
+ - name: Publish to PyPI
43
+ if: hashFiles('dist/*') != ''
44
+ uses: pypa/gh-action-pypi-publish@release/v1
45
+ with:
46
+ packages-dir: dist/
@@ -0,0 +1,46 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ *.egg
6
+ dist/
7
+ build/
8
+ .venv/
9
+
10
+ # Testing
11
+ .pytest_cache/
12
+ .coverage
13
+ htmlcov/
14
+
15
+ # Type checking / Linting
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+
19
+ # IDE
20
+ .vscode/
21
+ .idea/
22
+ *.swp
23
+ *.swo
24
+ *~
25
+
26
+ # AI Code Assistants
27
+ .claude/
28
+ .cursor/
29
+ .aider*
30
+ .codeium/
31
+ .continue/
32
+ .copilot/
33
+ .tabby/
34
+ CLAUDE.md
35
+ AGENTS.md
36
+
37
+ # OS
38
+ .DS_Store
39
+ Thumbs.db
40
+
41
+ # Environment
42
+ .env
43
+ .env.*
44
+
45
+ # official docs for reference
46
+ mocoapp-api-docs-master/
@@ -0,0 +1,6 @@
1
+ repos:
2
+ - repo: https://github.com/commitizen-tools/commitizen
3
+ rev: v4.13.9
4
+ hooks:
5
+ - id: commitizen
6
+ stages: [commit-msg]
@@ -0,0 +1,7 @@
1
+ # CHANGELOG
2
+
3
+ <!-- version list -->
4
+
5
+ ## v1.0.0 (2026-03-01)
6
+
7
+ - Initial Release
moco_py-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: moco-py
3
+ Version: 1.0.0
4
+ Summary: Python client library for the MOCO ERP API
5
+ Project-URL: Homepage, https://github.com/bandbyte/moco-py
6
+ Project-URL: Documentation, https://github.com/bandbyte/moco-py
7
+ Project-URL: Repository, https://github.com/bandbyte/moco-py
8
+ License-Expression: MIT
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: httpx>=0.27
11
+ Requires-Dist: pydantic>=2.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # moco-py
15
+
16
+ Python client library for the [MOCO ERP API](https://www.mocoapp.com).
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install moco-py
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```python
27
+ from moco_py import Moco
28
+
29
+ client = Moco(domain="yourcompany", api_key="your-api-key")
30
+ ```
@@ -0,0 +1,17 @@
1
+ # moco-py
2
+
3
+ Python client library for the [MOCO ERP API](https://www.mocoapp.com).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install moco-py
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from moco_py import Moco
15
+
16
+ client = Moco(domain="yourcompany", api_key="your-api-key")
17
+ ```
@@ -0,0 +1,72 @@
1
+ [project]
2
+ name = "moco-py"
3
+ version = "1.0.0"
4
+ description = "Python client library for the MOCO ERP API"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.10"
8
+ dependencies = [
9
+ "httpx>=0.27",
10
+ "pydantic>=2.0",
11
+ ]
12
+
13
+ [project.urls]
14
+ Homepage = "https://github.com/bandbyte/moco-py"
15
+ Documentation = "https://github.com/bandbyte/moco-py"
16
+ Repository = "https://github.com/bandbyte/moco-py"
17
+
18
+ [build-system]
19
+ requires = ["hatchling"]
20
+ build-backend = "hatchling.build"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/moco_py"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
27
+ markers = [
28
+ "integration: tests that hit a live MOCO API (require MOCO_API_KEY and MOCO_DOMAIN env vars)",
29
+ ]
30
+
31
+ [tool.ruff]
32
+ src = ["src"]
33
+ target-version = "py310"
34
+
35
+ [tool.ruff.lint]
36
+ select = ["E", "F", "I", "UP"]
37
+
38
+ [tool.mypy]
39
+ python_version = "3.10"
40
+ strict = true
41
+ packages = ["moco_py"]
42
+ mypy_path = "src"
43
+
44
+ [tool.commitizen]
45
+ name = "cz_conventional_commits"
46
+ version_provider = "pep621"
47
+ tag_format = "v$version"
48
+ update_changelog_on_bump = true
49
+
50
+ [tool.semantic_release]
51
+ version_toml = ["pyproject.toml:project.version"]
52
+ version_variables = ["src/moco_py/__init__.py:__version__"]
53
+ tag_format = "v{version}"
54
+ build_command = "uv build"
55
+
56
+ [tool.semantic_release.changelog.default_templates]
57
+ changelog_file = "CHANGELOG.md"
58
+
59
+ [tool.semantic_release.branches.main]
60
+ match = "main"
61
+
62
+ [dependency-groups]
63
+ dev = [
64
+ "commitizen>=4.13.9",
65
+ "mypy>=1.0",
66
+ "pre-commit>=4.5.1",
67
+ "pytest>=8.0",
68
+ "pytest-asyncio>=0.24",
69
+ "python-semantic-release>=10.5.3",
70
+ "respx>=0.22",
71
+ "ruff>=0.8",
72
+ ]
@@ -0,0 +1,36 @@
1
+ """MOCO ERP API client library."""
2
+
3
+ from ._pagination import AsyncPage, SyncPage
4
+ from ._response import MocoResponse
5
+ from .client import AsyncMoco, Moco
6
+ from .exceptions import (
7
+ APIConnectionError,
8
+ APITimeoutError,
9
+ AuthenticationError,
10
+ MocoError,
11
+ NotFoundError,
12
+ PermissionError,
13
+ RateLimitError,
14
+ ServerError,
15
+ ValidationError,
16
+ )
17
+
18
+ __version__ = "1.0.0"
19
+
20
+ __all__ = [
21
+ "AsyncMoco",
22
+ "AsyncPage",
23
+ "Moco",
24
+ "MocoResponse",
25
+ "SyncPage",
26
+ "__version__",
27
+ "APIConnectionError",
28
+ "APITimeoutError",
29
+ "AuthenticationError",
30
+ "MocoError",
31
+ "NotFoundError",
32
+ "PermissionError",
33
+ "RateLimitError",
34
+ "ServerError",
35
+ "ValidationError",
36
+ ]
@@ -0,0 +1,9 @@
1
+ """Internal constants for the MOCO API client."""
2
+
3
+ BASE_URL_TEMPLATE = "https://{domain}.mocoapp.com/api/v1"
4
+ ENV_API_KEY = "MOCO_API_KEY"
5
+ ENV_DOMAIN = "MOCO_DOMAIN"
6
+ DEFAULT_TIMEOUT = 30.0
7
+ DEFAULT_MAX_RETRIES = 2
8
+ DEFAULT_PER_PAGE = 100
9
+ RETRY_STATUS_CODES = {429, 500, 502, 503, 504}
@@ -0,0 +1,160 @@
1
+ """Header-based pagination for the MOCO API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import AsyncIterator, Iterator
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, Generic, TypeVar
9
+
10
+ import httpx
11
+ from pydantic import TypeAdapter
12
+
13
+ if TYPE_CHECKING:
14
+ from ._transport import AsyncTransport, SyncTransport
15
+
16
+ T = TypeVar("T")
17
+
18
+ _LINK_NEXT_RE = re.compile(r'<[^>]*[?&]page=(\d+)[^>]*>;\s*rel="next"')
19
+
20
+
21
+ def _parse_link_next(link: str | None) -> int | None:
22
+ """Extract the next page number from a Link header with rel="next"."""
23
+ if link is None:
24
+ return None
25
+ match = _LINK_NEXT_RE.search(link)
26
+ if match is None:
27
+ return None
28
+ return int(match.group(1))
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class PageInfo:
33
+ """Metadata from pagination response headers."""
34
+
35
+ page: int
36
+ per_page: int
37
+ total: int
38
+
39
+
40
+ class SyncPage(Generic[T]):
41
+ """A single page of results from a paginated MOCO API endpoint."""
42
+
43
+ def __init__(
44
+ self,
45
+ *,
46
+ items: list[T],
47
+ http_response: httpx.Response,
48
+ transport: SyncTransport,
49
+ path: str,
50
+ params: dict[str, object],
51
+ cast_to: type[T],
52
+ ) -> None:
53
+ self.items = items
54
+ self.http_response = http_response
55
+ self._transport = transport
56
+ self._path = path
57
+ self._params = params
58
+ self._cast_to = cast_to
59
+
60
+ headers = http_response.headers
61
+ self.page_info = PageInfo(
62
+ page=int(headers.get("X-Page", "1")),
63
+ per_page=int(headers.get("X-Per-Page", "100")),
64
+ total=int(headers.get("X-Total", str(len(items)))),
65
+ )
66
+ self._next_page = _parse_link_next(headers.get("Link"))
67
+
68
+ @property
69
+ def has_next(self) -> bool:
70
+ return self._next_page is not None
71
+
72
+ def next_page(self) -> SyncPage[T]:
73
+ """Fetch the next page of results."""
74
+ if self._next_page is None:
75
+ raise StopIteration("No more pages")
76
+ params = {**self._params, "page": self._next_page}
77
+ response = self._transport.request_raw("GET", self._path, params=params)
78
+ adapter = TypeAdapter(list[self._cast_to]) # type: ignore[name-defined]
79
+ items = adapter.validate_json(response.content)
80
+ return SyncPage(
81
+ items=items,
82
+ http_response=response,
83
+ transport=self._transport,
84
+ path=self._path,
85
+ params=self._params,
86
+ cast_to=self._cast_to,
87
+ )
88
+
89
+ def __iter__(self) -> Iterator[T]:
90
+ yield from self.items
91
+
92
+ def auto_paging_iter(self) -> Iterator[T]:
93
+ """Iterate over all items across all pages."""
94
+ page: SyncPage[T] = self
95
+ while True:
96
+ yield from page.items
97
+ if not page.has_next:
98
+ break
99
+ page = page.next_page()
100
+
101
+
102
+ class AsyncPage(Generic[T]):
103
+ """A single page of results from a paginated MOCO API endpoint (async)."""
104
+
105
+ def __init__(
106
+ self,
107
+ *,
108
+ items: list[T],
109
+ http_response: httpx.Response,
110
+ transport: AsyncTransport,
111
+ path: str,
112
+ params: dict[str, object],
113
+ cast_to: type[T],
114
+ ) -> None:
115
+ self.items = items
116
+ self.http_response = http_response
117
+ self._transport = transport
118
+ self._path = path
119
+ self._params = params
120
+ self._cast_to = cast_to
121
+
122
+ headers = http_response.headers
123
+ self.page_info = PageInfo(
124
+ page=int(headers.get("X-Page", "1")),
125
+ per_page=int(headers.get("X-Per-Page", "100")),
126
+ total=int(headers.get("X-Total", str(len(items)))),
127
+ )
128
+ self._next_page = _parse_link_next(headers.get("Link"))
129
+
130
+ @property
131
+ def has_next(self) -> bool:
132
+ return self._next_page is not None
133
+
134
+ async def next_page(self) -> AsyncPage[T]:
135
+ if self._next_page is None:
136
+ raise StopAsyncIteration("No more pages")
137
+ params = {**self._params, "page": self._next_page}
138
+ response = await self._transport.request_raw("GET", self._path, params=params)
139
+ adapter = TypeAdapter(list[self._cast_to]) # type: ignore[name-defined]
140
+ items = adapter.validate_json(response.content)
141
+ return AsyncPage(
142
+ items=items,
143
+ http_response=response,
144
+ transport=self._transport,
145
+ path=self._path,
146
+ params=self._params,
147
+ cast_to=self._cast_to,
148
+ )
149
+
150
+ def __iter__(self) -> Iterator[T]:
151
+ yield from self.items
152
+
153
+ async def auto_paging_iter(self) -> AsyncIterator[T]:
154
+ page: AsyncPage[T] = self
155
+ while True:
156
+ for item in page.items:
157
+ yield item
158
+ if not page.has_next:
159
+ break
160
+ page = await page.next_page()
@@ -0,0 +1,157 @@
1
+ """Base resource classes for sync and async API access."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, TypeVar
6
+
7
+ from pydantic import TypeAdapter
8
+
9
+ from ._pagination import AsyncPage, SyncPage
10
+ from ._response import MocoResponse
11
+ from ._transport import AsyncTransport, SyncTransport
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ class SyncResource:
17
+ """Base class for synchronous API resource namespaces."""
18
+
19
+ def __init__(self, transport: SyncTransport) -> None:
20
+ self._transport = transport
21
+
22
+ def _get(
23
+ self,
24
+ path: str,
25
+ *,
26
+ params: dict[str, Any] | None = None,
27
+ cast_to: type[T],
28
+ ) -> MocoResponse[T]:
29
+ return self._transport.request("GET", path, params=params, cast_to=cast_to)
30
+
31
+ def _get_list(
32
+ self,
33
+ path: str,
34
+ *,
35
+ params: dict[str, Any] | None = None,
36
+ cast_to: type[T],
37
+ ) -> SyncPage[T]:
38
+ response = self._transport.request_raw("GET", path, params=params)
39
+ adapter = TypeAdapter(list[cast_to]) # type: ignore[valid-type]
40
+ items = adapter.validate_json(response.content)
41
+ return SyncPage(
42
+ items=items,
43
+ http_response=response,
44
+ transport=self._transport,
45
+ path=path,
46
+ params=params or {},
47
+ cast_to=cast_to,
48
+ )
49
+
50
+ def _post(
51
+ self,
52
+ path: str,
53
+ *,
54
+ json_data: Any = None,
55
+ cast_to: type[T],
56
+ ) -> MocoResponse[T]:
57
+ return self._transport.request(
58
+ "POST", path, json_data=json_data, cast_to=cast_to
59
+ )
60
+
61
+ def _put(
62
+ self,
63
+ path: str,
64
+ *,
65
+ json_data: Any = None,
66
+ cast_to: type[T],
67
+ ) -> MocoResponse[T]:
68
+ return self._transport.request(
69
+ "PUT", path, json_data=json_data, cast_to=cast_to
70
+ )
71
+
72
+ def _patch(
73
+ self,
74
+ path: str,
75
+ *,
76
+ json_data: Any = None,
77
+ cast_to: type[T],
78
+ ) -> MocoResponse[T]:
79
+ return self._transport.request(
80
+ "PATCH", path, json_data=json_data, cast_to=cast_to
81
+ )
82
+
83
+ def _delete(self, path: str) -> MocoResponse[None]:
84
+ return self._transport.request("DELETE", path, cast_to=None)
85
+
86
+
87
+ class AsyncResource:
88
+ """Base class for asynchronous API resource namespaces."""
89
+
90
+ def __init__(self, transport: AsyncTransport) -> None:
91
+ self._transport = transport
92
+
93
+ async def _get(
94
+ self,
95
+ path: str,
96
+ *,
97
+ params: dict[str, Any] | None = None,
98
+ cast_to: type[T],
99
+ ) -> MocoResponse[Any]:
100
+ return await self._transport.request(
101
+ "GET", path, params=params, cast_to=cast_to
102
+ )
103
+
104
+ async def _get_list(
105
+ self,
106
+ path: str,
107
+ *,
108
+ params: dict[str, Any] | None = None,
109
+ cast_to: type[T],
110
+ ) -> AsyncPage[T]:
111
+ response = await self._transport.request_raw("GET", path, params=params)
112
+ adapter = TypeAdapter(list[cast_to]) # type: ignore[valid-type]
113
+ items = adapter.validate_json(response.content)
114
+ return AsyncPage(
115
+ items=items,
116
+ http_response=response,
117
+ transport=self._transport,
118
+ path=path,
119
+ params=params or {},
120
+ cast_to=cast_to,
121
+ )
122
+
123
+ async def _post(
124
+ self,
125
+ path: str,
126
+ *,
127
+ json_data: Any = None,
128
+ cast_to: type[T],
129
+ ) -> MocoResponse[Any]:
130
+ return await self._transport.request(
131
+ "POST", path, json_data=json_data, cast_to=cast_to
132
+ )
133
+
134
+ async def _put(
135
+ self,
136
+ path: str,
137
+ *,
138
+ json_data: Any = None,
139
+ cast_to: type[T],
140
+ ) -> MocoResponse[Any]:
141
+ return await self._transport.request(
142
+ "PUT", path, json_data=json_data, cast_to=cast_to
143
+ )
144
+
145
+ async def _patch(
146
+ self,
147
+ path: str,
148
+ *,
149
+ json_data: Any = None,
150
+ cast_to: type[T],
151
+ ) -> MocoResponse[Any]:
152
+ return await self._transport.request(
153
+ "PATCH", path, json_data=json_data, cast_to=cast_to
154
+ )
155
+
156
+ async def _delete(self, path: str) -> MocoResponse[Any]:
157
+ return await self._transport.request("DELETE", path, cast_to=None)
@@ -0,0 +1,27 @@
1
+ """Response wrapper providing typed access alongside raw httpx.Response."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Generic, TypeVar
6
+
7
+ import httpx
8
+
9
+ T = TypeVar("T")
10
+
11
+
12
+ class MocoResponse(Generic[T]):
13
+ """Wraps a parsed Pydantic model with access to the raw HTTP response."""
14
+
15
+ __slots__ = ("parsed", "http_response")
16
+
17
+ def __init__(self, *, parsed: T, http_response: httpx.Response) -> None:
18
+ self.parsed = parsed
19
+ self.http_response = http_response
20
+
21
+ @property
22
+ def status_code(self) -> int:
23
+ return self.http_response.status_code
24
+
25
+ @property
26
+ def headers(self) -> httpx.Headers:
27
+ return self.http_response.headers