bapp-api-client 0.2.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.
@@ -0,0 +1,53 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write # Required for trusted publishing
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 2
16
+
17
+ - name: Check if version changed
18
+ id: version
19
+ run: |
20
+ NEW_VERSION=$(python -c "
21
+ import tomllib
22
+ with open('pyproject.toml', 'rb') as f:
23
+ print(tomllib.load(f)['project']['version'])
24
+ ")
25
+ OLD_VERSION=$(git show HEAD~1:pyproject.toml | python -c "
26
+ import tomllib, sys
27
+ print(tomllib.load(sys.stdin.buffer)['project']['version'])
28
+ " 2>/dev/null || echo "")
29
+ echo "new=$NEW_VERSION" >> "$GITHUB_OUTPUT"
30
+ echo "old=$OLD_VERSION" >> "$GITHUB_OUTPUT"
31
+ if [ "$NEW_VERSION" != "$OLD_VERSION" ]; then
32
+ echo "changed=true" >> "$GITHUB_OUTPUT"
33
+ echo "Version changed: $OLD_VERSION -> $NEW_VERSION"
34
+ else
35
+ echo "changed=false" >> "$GITHUB_OUTPUT"
36
+ echo "Version unchanged: $NEW_VERSION"
37
+ fi
38
+
39
+ - name: Set up Python
40
+ if: steps.version.outputs.changed == 'true'
41
+ uses: actions/setup-python@v5
42
+ with:
43
+ python-version: "3.12"
44
+
45
+ - name: Build package
46
+ if: steps.version.outputs.changed == 'true'
47
+ run: |
48
+ pip install build
49
+ python -m build
50
+
51
+ - name: Publish to PyPI
52
+ if: steps.version.outputs.changed == 'true'
53
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ - Initial release.
6
+ - Authentication: Bearer (JWT/OAuth) and Token (API key).
7
+ - Entity CRUD: list, get, create, update, patch, delete.
8
+ - Paginated list responses with metadata (count, next, previous).
9
+ - Entity introspection: list_introspect, detail_introspect.
10
+ - Tasks: list, detail, run (sync and async with polling).
11
+ - Long-running task support via run_task_async with automatic polling.
12
+ - File uploads: automatic multipart/form-data detection.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BAPP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: bapp-api-client
3
+ Version: 0.2.0
4
+ Summary: BAPP Auto API Client for Python
5
+ Project-URL: Homepage, https://www.bapp.ro
6
+ Project-URL: Repository, https://github.com/bapp-open/sdk-python
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: api,bapp,client
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: requests>=2.20
@@ -0,0 +1,158 @@
1
+ # BAPP Auto API Client — Python
2
+
3
+ Official Python client for the [BAPP Auto API](https://www.bapp.ro). Provides a
4
+ simple, consistent interface for authentication, entity CRUD, and task execution.
5
+
6
+ ## Getting Started
7
+
8
+ ### 1. Install
9
+
10
+ ```bash
11
+ pip install bapp-api-client
12
+ ```
13
+
14
+ ### 2. Create a client
15
+
16
+ ```python
17
+ from bapp_api_client import BappApiClient
18
+
19
+ client = BappApiClient(token="your-api-key")
20
+ ```
21
+
22
+ ### 3. Make your first request
23
+
24
+ ```python
25
+ # List with filters
26
+ countries = client.list("core.country", page=1, search="Romania")
27
+
28
+ # Get by ID
29
+ country = client.get("core.country", "42")
30
+
31
+ # Create
32
+ new = client.create("core.country", {"name": "Romania", "code": "RO"})
33
+
34
+ # Update (full)
35
+ client.update("core.country", "42", {"name": "Romania", "code": "RO"})
36
+
37
+ # Patch (partial)
38
+ client.patch("core.country", "42", {"code": "RO"})
39
+
40
+ # Delete
41
+ client.delete("core.country", "42")
42
+ ```
43
+
44
+ ## Authentication
45
+
46
+ The client supports **Token** (API key) and **Bearer** (JWT / OAuth) authentication.
47
+ Token auth already includes a tenant binding, so you don't need to specify `tenant` separately.
48
+
49
+ ```python
50
+ # Static API token (tenant is included in the token)
51
+ client = BappApiClient(token="your-api-key")
52
+
53
+ # Bearer (JWT / OAuth)
54
+ client = BappApiClient(bearer="eyJhbG...", tenant="1")
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ `tenant` and `app` can be changed at any time after construction:
60
+
61
+ ```python
62
+ client.tenant = "2"
63
+ client.app = "wms"
64
+ ```
65
+
66
+ ## API Reference
67
+
68
+ ### Client options
69
+
70
+ | Option | Description | Default |
71
+ |--------|-------------|---------|
72
+ | `token` | Static API token (`Token <value>`) — includes tenant | — |
73
+ | `bearer` | Bearer / JWT token | — |
74
+ | `host` | API base URL | `https://panel.bapp.ro/api` |
75
+ | `tenant` | Tenant ID (`x-tenant-id` header) | `None` |
76
+ | `app` | App slug (`x-app-slug` header) | `"account"` |
77
+
78
+ ### Methods
79
+
80
+ | Method | Description |
81
+ |--------|-------------|
82
+ | `me()` | Get current user profile |
83
+ | `get_app(app_slug)` | Get app configuration by slug |
84
+ | `list(content_type, **filters)` | List entities (paginated) |
85
+ | `get(content_type, id)` | Get a single entity |
86
+ | `create(content_type, data)` | Create an entity |
87
+ | `update(content_type, id, data)` | Full update (PUT) |
88
+ | `patch(content_type, id, data)` | Partial update (PATCH) |
89
+ | `delete(content_type, id)` | Delete an entity |
90
+ | `list_introspect(content_type)` | Get list view metadata |
91
+ | `detail_introspect(content_type)` | Get detail view metadata |
92
+ | `list_tasks()` | List available task codes |
93
+ | `detail_task(code)` | Get task configuration |
94
+ | `run_task(code, payload?)` | Execute a task |
95
+ | `run_task_async(code, payload?)` | Run a long-running task and poll until done |
96
+
97
+ ### Paginated responses
98
+
99
+ `list()` returns the results directly as a list/array. Pagination metadata is
100
+ available as extra attributes:
101
+
102
+ - `count` — total number of items across all pages
103
+ - `next` — URL of the next page (or `null`)
104
+ - `previous` — URL of the previous page (or `null`)
105
+
106
+ ## File Uploads
107
+
108
+ When data contains file objects, the client automatically switches from JSON to
109
+ `multipart/form-data`. Mix regular fields and files in the same call:
110
+
111
+ ```python
112
+ # File objects, byte strings, or tuples (filename, file) are auto-detected
113
+ client.create("myapp.document", {
114
+ "name": "Report",
115
+ "file": open("report.pdf", "rb"),
116
+ })
117
+
118
+ # Tuple form for explicit filename / content-type
119
+ client.create("myapp.document", {
120
+ "name": "Report",
121
+ "file": ("report.pdf", open("report.pdf", "rb"), "application/pdf"),
122
+ })
123
+
124
+ # Also works with tasks
125
+ client.run_task("myapp.import_data", {
126
+ "format": "csv",
127
+ "file": open("data.csv", "rb"),
128
+ })
129
+ ```
130
+
131
+ ## Tasks
132
+
133
+ Tasks are server-side actions identified by a dotted code (e.g. `myapp.export_report`).
134
+
135
+ ```python
136
+ # List all tasks
137
+ tasks = client.list_tasks()
138
+
139
+ # Inspect a task
140
+ cfg = client.detail_task("myapp.export_report")
141
+
142
+ # Run without payload (GET)
143
+ result = client.run_task("myapp.export_report")
144
+
145
+ # Run with payload (POST)
146
+ result = client.run_task("myapp.export_report", {"format": "csv"})
147
+ ```
148
+
149
+ ### Long-running tasks
150
+
151
+ Some tasks run asynchronously on the server. When triggered, they return an `id`
152
+ that can be polled via `bapp_framework.taskdata`. Use `run_task_async()` to
153
+ handle this automatically — it polls until `finished` is `true` and returns the
154
+ final task data (which includes a `file` URL when the task produces a download).
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1,3 @@
1
+ from .client import BappApiClient, PagedList
2
+
3
+ __all__ = ["BappApiClient", "PagedList"]
@@ -0,0 +1,232 @@
1
+ """BAPP Auto API Client for Python."""
2
+
3
+ import io
4
+ import time
5
+ import requests
6
+
7
+
8
+ def _has_files(data):
9
+ """Return True if data dict contains file-like values."""
10
+ if not isinstance(data, dict):
11
+ return False
12
+ for v in data.values():
13
+ if isinstance(v, (io.IOBase, bytes, bytearray)):
14
+ return True
15
+ if hasattr(v, "read"):
16
+ return True
17
+ if isinstance(v, tuple) and len(v) >= 2:
18
+ return True
19
+ return False
20
+
21
+
22
+ class PagedList(list):
23
+ """A list of results with pagination metadata.
24
+
25
+ Behaves like a normal list (iterating, indexing, len) but also exposes
26
+ ``count``, ``next``, and ``previous`` from the paginated API response.
27
+ """
28
+
29
+ def __init__(self, results, *, count=0, next=None, previous=None):
30
+ super().__init__(results)
31
+ self.count = count
32
+ self.next = next
33
+ self.previous = previous
34
+
35
+ def __repr__(self):
36
+ return f"PagedList(count={self.count}, len={len(self)})"
37
+
38
+
39
+ class BappApiClient:
40
+ """Client for the BAPP Auto API.
41
+
42
+ Args:
43
+ bearer: Bearer token for authentication.
44
+ token: Token-based authentication (``Token <value>``).
45
+ host: Base URL of the API.
46
+ tenant: Default tenant ID sent as ``x-tenant-id`` header.
47
+ app: Default app slug sent as ``x-app-slug`` header.
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ bearer=None,
53
+ token=None,
54
+ host="https://panel.bapp.ro/api",
55
+ tenant=None,
56
+ app="account",
57
+ ):
58
+ self.host = host.rstrip("/")
59
+ self.tenant = tenant
60
+ self.app = app
61
+ self._session = requests.Session()
62
+ if bearer:
63
+ self._session.headers["Authorization"] = f"Bearer {bearer}"
64
+ elif token:
65
+ self._session.headers["Authorization"] = f"Token {token}"
66
+
67
+ # -- internals -----------------------------------------------------------
68
+
69
+ def _headers(self, extra=None):
70
+ h = {}
71
+ if self.tenant is not None:
72
+ h["x-tenant-id"] = str(self.tenant)
73
+ if self.app is not None:
74
+ h["x-app-slug"] = self.app
75
+ if extra:
76
+ h.update(extra)
77
+ return h
78
+
79
+ def _request(self, method, path, params=None, json=None, headers=None):
80
+ kwargs = {}
81
+ if json is not None and _has_files(json):
82
+ files = {}
83
+ data = {}
84
+ for k, v in json.items():
85
+ if isinstance(v, (io.IOBase, bytes, bytearray)) or hasattr(v, "read"):
86
+ files[k] = v
87
+ elif isinstance(v, tuple) and len(v) >= 2:
88
+ files[k] = v
89
+ else:
90
+ data[k] = v
91
+ kwargs["files"] = files
92
+ kwargs["data"] = data
93
+ else:
94
+ kwargs["json"] = json
95
+ resp = self._session.request(
96
+ method,
97
+ f"{self.host}{path}",
98
+ params=params,
99
+ headers=self._headers(headers),
100
+ **kwargs,
101
+ )
102
+ resp.raise_for_status()
103
+ if resp.status_code == 204:
104
+ return None
105
+ return resp.json()
106
+
107
+ # -- user ----------------------------------------------------------------
108
+
109
+ def me(self):
110
+ """Get current user profile."""
111
+ return self._request("GET", "/tasks/bapp_framework.me", headers={"x-app-slug": ""})
112
+
113
+ # -- app -----------------------------------------------------------------
114
+
115
+ def get_app(self, app_slug):
116
+ """Get app configuration by slug."""
117
+ return self._request(
118
+ "GET", "/tasks/bapp_framework.getapp", headers={"x-app-slug": app_slug}
119
+ )
120
+
121
+ # -- entity introspect ---------------------------------------------------
122
+
123
+ def list_introspect(self, content_type):
124
+ """Get entity list introspect for a content type."""
125
+ return self._request(
126
+ "GET", "/tasks/bapp_framework.listintrospect", params={"ct": content_type}
127
+ )
128
+
129
+ def detail_introspect(self, content_type, pk=None):
130
+ """Get entity detail introspect for a content type."""
131
+ params = {"ct": content_type}
132
+ if pk is not None:
133
+ params["pk"] = pk
134
+ return self._request(
135
+ "GET", "/tasks/bapp_framework.detailintrospect", params=params
136
+ )
137
+
138
+ # -- entity CRUD ---------------------------------------------------------
139
+
140
+ def list(self, content_type, **filters):
141
+ """List entities of a content type with optional filters.
142
+
143
+ Returns a ``PagedList`` — a regular list of results with additional
144
+ ``.count``, ``.next``, and ``.previous`` attributes.
145
+ """
146
+ data = self._request(
147
+ "GET", f"/content-type/{content_type}/", params=filters or None
148
+ )
149
+ return PagedList(
150
+ data.get("results", []),
151
+ count=data.get("count", 0),
152
+ next=data.get("next"),
153
+ previous=data.get("previous"),
154
+ )
155
+
156
+ def get(self, content_type, id):
157
+ """Get a single entity by content type and ID."""
158
+ return self._request("GET", f"/content-type/{content_type}/{id}/")
159
+
160
+ def create(self, content_type, data=None):
161
+ """Create a new entity of a content type."""
162
+ return self._request("POST", f"/content-type/{content_type}/", json=data)
163
+
164
+ def update(self, content_type, id, data=None):
165
+ """Full update of an entity."""
166
+ return self._request("PUT", f"/content-type/{content_type}/{id}/", json=data)
167
+
168
+ def patch(self, content_type, id, data=None):
169
+ """Partial update of an entity."""
170
+ return self._request("PATCH", f"/content-type/{content_type}/{id}/", json=data)
171
+
172
+ def delete(self, content_type, id):
173
+ """Delete an entity."""
174
+ return self._request("DELETE", f"/content-type/{content_type}/{id}/")
175
+
176
+ # -- tasks ---------------------------------------------------------------
177
+
178
+ def list_tasks(self):
179
+ """List all available task codes."""
180
+ return self._request("GET", "/tasks")
181
+
182
+ def detail_task(self, code):
183
+ """Get task configuration by code."""
184
+ return self._request("OPTIONS", f"/tasks/{code}")
185
+
186
+ def run_task(self, code, payload=None):
187
+ """Run a task. Uses GET when no payload, POST otherwise."""
188
+ if payload is None:
189
+ return self._request("GET", f"/tasks/{code}")
190
+ return self._request("POST", f"/tasks/{code}", json=payload)
191
+
192
+ def run_task_async(self, code, payload=None, poll_interval=1, timeout=300):
193
+ """Run a long-running task and poll until finished.
194
+
195
+ When the task returns an ``id``, the method polls
196
+ ``bapp_framework.taskdata`` until the task finishes (or fails).
197
+ Returns the final task data dict which includes ``file`` when
198
+ the task produces a downloadable file.
199
+
200
+ Args:
201
+ code: Task code.
202
+ payload: Task payload (triggers POST).
203
+ poll_interval: Seconds between polls (default 1).
204
+ timeout: Max seconds to wait (default 300).
205
+
206
+ Raises:
207
+ TimeoutError: If the task doesn't finish within *timeout*.
208
+ RuntimeError: If the task reports failure.
209
+ """
210
+ result = self.run_task(code, payload)
211
+ task_id = result.get("id") if isinstance(result, dict) else None
212
+ if task_id is None:
213
+ return result
214
+
215
+ deadline = time.monotonic() + timeout
216
+ while time.monotonic() < deadline:
217
+ time.sleep(poll_interval)
218
+ page = self._request(
219
+ "GET", "/content-type/bapp_framework.taskdata/",
220
+ params={"id": task_id},
221
+ )
222
+ results = page.get("results", [])
223
+ if not results:
224
+ continue
225
+ task_data = results[0]
226
+ if task_data.get("failed"):
227
+ raise RuntimeError(
228
+ f"Task {code} failed: {task_data.get('message', '')}"
229
+ )
230
+ if task_data.get("finished"):
231
+ return task_data
232
+ raise TimeoutError(f"Task {code} ({task_id}) did not finish within {timeout}s")
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bapp-api-client"
7
+ version = "0.2.0"
8
+ description = "BAPP Auto API Client for Python"
9
+ requires-python = ">=3.9"
10
+ license = "MIT"
11
+ keywords = ["bapp", "api", "client"]
12
+ dependencies = ["requests>=2.20"]
13
+
14
+ [project.urls]
15
+ Homepage = "https://www.bapp.ro"
16
+ Repository = "https://github.com/bapp-open/sdk-python"