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.
- bapp_api_client-0.2.0/.github/workflows/publish.yml +53 -0
- bapp_api_client-0.2.0/CHANGELOG.md +12 -0
- bapp_api_client-0.2.0/LICENSE +21 -0
- bapp_api_client-0.2.0/PKG-INFO +11 -0
- bapp_api_client-0.2.0/README.md +158 -0
- bapp_api_client-0.2.0/bapp_api_client/__init__.py +3 -0
- bapp_api_client-0.2.0/bapp_api_client/client.py +232 -0
- bapp_api_client-0.2.0/pyproject.toml +16 -0
|
@@ -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,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"
|