ineepy 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.
- ineepy-0.1.0/PKG-INFO +151 -0
- ineepy-0.1.0/README.md +139 -0
- ineepy-0.1.0/pyproject.toml +77 -0
- ineepy-0.1.0/src/ineepy/__init__.py +4 -0
- ineepy-0.1.0/src/ineepy/_auth.py +143 -0
- ineepy-0.1.0/src/ineepy/_client.py +143 -0
- ineepy-0.1.0/src/ineepy/_config.py +58 -0
- ineepy-0.1.0/src/ineepy/_exceptions.py +74 -0
- ineepy-0.1.0/src/ineepy/_models.py +278 -0
- ineepy-0.1.0/src/ineepy/_resources/__init__.py +0 -0
- ineepy-0.1.0/src/ineepy/_resources/_base.py +59 -0
- ineepy-0.1.0/src/ineepy/_resources/api_keys.py +184 -0
- ineepy-0.1.0/src/ineepy/_resources/data.py +565 -0
- ineepy-0.1.0/src/ineepy/_resources/namespaces.py +277 -0
- ineepy-0.1.0/src/ineepy/_resources/tables.py +448 -0
- ineepy-0.1.0/src/ineepy/_resources/users.py +252 -0
- ineepy-0.1.0/src/ineepy/exceptions.py +32 -0
- ineepy-0.1.0/src/ineepy/helpers.py +25 -0
- ineepy-0.1.0/src/ineepy/models.py +22 -0
ineepy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ineepy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: cmts
|
|
6
|
+
Author-email: cmts <cmts@noreply.codeberg.org>
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: pydantic>=2.13.4
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.14.1
|
|
10
|
+
Requires-Python: >=3.14
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# ineepy
|
|
14
|
+
|
|
15
|
+
[](https://woodpecker-ineep.exe.xyz:8000/repos/4)
|
|
16
|
+
|
|
17
|
+
A Python client library for the [ineep](https://codeberg.org/cmts/ineep) lakehouse API.
|
|
18
|
+
|
|
19
|
+
## Links
|
|
20
|
+
|
|
21
|
+
- [Documentation](https://ineep.codeberg.page/ineepy/)
|
|
22
|
+
- [Repository](https://codeberg.org/Ineep/ineepy)
|
|
23
|
+
- [Changelog](./CHANGELOG.md)
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Python 3.14+
|
|
28
|
+
- [`uv`](https://docs.astral.sh/uv/)
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv add ineepy
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or for development:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone <repo-url>
|
|
40
|
+
cd ineepy
|
|
41
|
+
uv sync
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Authentication
|
|
47
|
+
|
|
48
|
+
Obtain a JWT token before creating a client:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from ineepy import get_token, async_get_token
|
|
52
|
+
|
|
53
|
+
# Synchronous
|
|
54
|
+
token = get_token(email='user@example.com', password='secret')
|
|
55
|
+
|
|
56
|
+
# Async
|
|
57
|
+
token = await async_get_token(email='user@example.com', password='secret')
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The `scope` parameter defaults to `'user'`. Use `'admin'` for administrative operations.
|
|
61
|
+
|
|
62
|
+
### Synchronous client
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from ineepy import Ineepy, get_token
|
|
66
|
+
|
|
67
|
+
token = get_token(email='user@example.com', password='secret')
|
|
68
|
+
|
|
69
|
+
with Ineepy(token=token) as client:
|
|
70
|
+
user, etag = client.users.me()
|
|
71
|
+
namespaces = client.namespaces.list()
|
|
72
|
+
tables = client.tables.list(namespace_id='my-namespace')
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Async client
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from ineepy import AsyncIneepy, async_get_token
|
|
79
|
+
|
|
80
|
+
token = await async_get_token(email='user@example.com', password='secret')
|
|
81
|
+
|
|
82
|
+
async with AsyncIneepy(token=token) as client:
|
|
83
|
+
user, etag = await client.users.me()
|
|
84
|
+
namespaces = await client.namespaces.list()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Configuration
|
|
88
|
+
|
|
89
|
+
The base URL defaults to `http://localhost:8000` and can be overridden via environment variable or constructor argument:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export INEEPY_BASE_URL=https://my-server.example.com
|
|
93
|
+
export INEEPY_API_VERSION=v1 # default
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
client = Ineepy(base_url='https://my-server.example.com', token=token)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Resources
|
|
101
|
+
|
|
102
|
+
| Resource | Access | Description |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `users` | `client.users` | User management (list, get, create, update, deactivate) |
|
|
105
|
+
| `api_keys` | `client.api_keys` | API key management (create, list, revoke) |
|
|
106
|
+
| `namespaces` | `client.namespaces` | Namespace management (list, get, create, update, delete) |
|
|
107
|
+
| `tables` | `client.tables` | Table schema management within namespaces |
|
|
108
|
+
| `data` | `client.data` | Data upload, download, update, query, and job polling |
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
make format # format code
|
|
114
|
+
make lint # lint code
|
|
115
|
+
make test # run unit tests with coverage
|
|
116
|
+
make test-e2e # run end-to-end tests against a live server
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### E2E tests
|
|
120
|
+
|
|
121
|
+
E2E tests require a running ineep server and admin credentials:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
cp .env.example .env
|
|
125
|
+
# edit .env with your credentials
|
|
126
|
+
make test-e2e
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The `.env` file is gitignored. See `.env.example` for the available variables.
|
|
130
|
+
|
|
131
|
+
## Project structure
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
src/ineepy/ Main package
|
|
135
|
+
_auth.py Token acquisition (sync + async)
|
|
136
|
+
_client.py Ineepy and AsyncIneepy clients
|
|
137
|
+
_config.py Pydantic settings (env vars)
|
|
138
|
+
_exceptions.py Exception hierarchy
|
|
139
|
+
_models.py Pydantic response models
|
|
140
|
+
tests/
|
|
141
|
+
resources/ Unit tests per resource
|
|
142
|
+
e2e/ End-to-end workflow tests
|
|
143
|
+
scripts/ Shell scripts called by make
|
|
144
|
+
pyproject.toml Project metadata and tool config
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
This project is licensed under the GNU General Public License v3.0.
|
|
150
|
+
|
|
151
|
+
You are free to use, modify, and distribute this software under the terms of the GPL v3. Any derivative work must also be distributed under the same license. See the LICENSE file for the full license text.
|
ineepy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# ineepy
|
|
2
|
+
|
|
3
|
+
[](https://woodpecker-ineep.exe.xyz:8000/repos/4)
|
|
4
|
+
|
|
5
|
+
A Python client library for the [ineep](https://codeberg.org/cmts/ineep) lakehouse API.
|
|
6
|
+
|
|
7
|
+
## Links
|
|
8
|
+
|
|
9
|
+
- [Documentation](https://ineep.codeberg.page/ineepy/)
|
|
10
|
+
- [Repository](https://codeberg.org/Ineep/ineepy)
|
|
11
|
+
- [Changelog](./CHANGELOG.md)
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Python 3.14+
|
|
16
|
+
- [`uv`](https://docs.astral.sh/uv/)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv add ineepy
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or for development:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git clone <repo-url>
|
|
28
|
+
cd ineepy
|
|
29
|
+
uv sync
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Authentication
|
|
35
|
+
|
|
36
|
+
Obtain a JWT token before creating a client:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from ineepy import get_token, async_get_token
|
|
40
|
+
|
|
41
|
+
# Synchronous
|
|
42
|
+
token = get_token(email='user@example.com', password='secret')
|
|
43
|
+
|
|
44
|
+
# Async
|
|
45
|
+
token = await async_get_token(email='user@example.com', password='secret')
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The `scope` parameter defaults to `'user'`. Use `'admin'` for administrative operations.
|
|
49
|
+
|
|
50
|
+
### Synchronous client
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from ineepy import Ineepy, get_token
|
|
54
|
+
|
|
55
|
+
token = get_token(email='user@example.com', password='secret')
|
|
56
|
+
|
|
57
|
+
with Ineepy(token=token) as client:
|
|
58
|
+
user, etag = client.users.me()
|
|
59
|
+
namespaces = client.namespaces.list()
|
|
60
|
+
tables = client.tables.list(namespace_id='my-namespace')
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Async client
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from ineepy import AsyncIneepy, async_get_token
|
|
67
|
+
|
|
68
|
+
token = await async_get_token(email='user@example.com', password='secret')
|
|
69
|
+
|
|
70
|
+
async with AsyncIneepy(token=token) as client:
|
|
71
|
+
user, etag = await client.users.me()
|
|
72
|
+
namespaces = await client.namespaces.list()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Configuration
|
|
76
|
+
|
|
77
|
+
The base URL defaults to `http://localhost:8000` and can be overridden via environment variable or constructor argument:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export INEEPY_BASE_URL=https://my-server.example.com
|
|
81
|
+
export INEEPY_API_VERSION=v1 # default
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
client = Ineepy(base_url='https://my-server.example.com', token=token)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Resources
|
|
89
|
+
|
|
90
|
+
| Resource | Access | Description |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `users` | `client.users` | User management (list, get, create, update, deactivate) |
|
|
93
|
+
| `api_keys` | `client.api_keys` | API key management (create, list, revoke) |
|
|
94
|
+
| `namespaces` | `client.namespaces` | Namespace management (list, get, create, update, delete) |
|
|
95
|
+
| `tables` | `client.tables` | Table schema management within namespaces |
|
|
96
|
+
| `data` | `client.data` | Data upload, download, update, query, and job polling |
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
make format # format code
|
|
102
|
+
make lint # lint code
|
|
103
|
+
make test # run unit tests with coverage
|
|
104
|
+
make test-e2e # run end-to-end tests against a live server
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### E2E tests
|
|
108
|
+
|
|
109
|
+
E2E tests require a running ineep server and admin credentials:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
cp .env.example .env
|
|
113
|
+
# edit .env with your credentials
|
|
114
|
+
make test-e2e
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The `.env` file is gitignored. See `.env.example` for the available variables.
|
|
118
|
+
|
|
119
|
+
## Project structure
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
src/ineepy/ Main package
|
|
123
|
+
_auth.py Token acquisition (sync + async)
|
|
124
|
+
_client.py Ineepy and AsyncIneepy clients
|
|
125
|
+
_config.py Pydantic settings (env vars)
|
|
126
|
+
_exceptions.py Exception hierarchy
|
|
127
|
+
_models.py Pydantic response models
|
|
128
|
+
tests/
|
|
129
|
+
resources/ Unit tests per resource
|
|
130
|
+
e2e/ End-to-end workflow tests
|
|
131
|
+
scripts/ Shell scripts called by make
|
|
132
|
+
pyproject.toml Project metadata and tool config
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
This project is licensed under the GNU General Public License v3.0.
|
|
138
|
+
|
|
139
|
+
You are free to use, modify, and distribute this software under the terms of the GPL v3. Any derivative work must also be distributed under the same license. See the LICENSE file for the full license text.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ineepy"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "cmts", email = "cmts@noreply.codeberg.org" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx>=0.28.1",
|
|
12
|
+
"pydantic>=2.13.4",
|
|
13
|
+
"pydantic-settings>=2.14.1",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
ineepy = "ineepy:main"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["uv_build>=0.11.7,<0.12.0"]
|
|
21
|
+
build-backend = "uv_build"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"marimo>=0.23.6",
|
|
26
|
+
"mkdocstrings-python>=2.0.3",
|
|
27
|
+
"pytest>=9.0.2",
|
|
28
|
+
"pytest-asyncio>=1.3.0",
|
|
29
|
+
"pytest-cov>=7.0.0",
|
|
30
|
+
"pytest-httpx>=0.36.2",
|
|
31
|
+
"ruff>=0.15.5",
|
|
32
|
+
"zensical>=0.0.42",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
pythonpath = ['.']
|
|
37
|
+
asyncio_mode = 'auto'
|
|
38
|
+
markers = [
|
|
39
|
+
"e2e: end-to-end tests requiring a live server (exclude with -m 'not e2e')",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.coverage.run]
|
|
43
|
+
source = ['ineepy']
|
|
44
|
+
branch = true
|
|
45
|
+
|
|
46
|
+
[tool.coverage.report]
|
|
47
|
+
show_missing = true
|
|
48
|
+
skip_covered = false
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 79
|
|
52
|
+
src = ["src"]
|
|
53
|
+
extend-exclude = ["tests"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
preview = true
|
|
57
|
+
select = ["F", "E", "W", "N", "PL", "PT", "I001"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
"tests/**/*.py" = [
|
|
61
|
+
"E501", # long decorative comment lines in test files
|
|
62
|
+
"F401", # unused imports (test fixtures may import for re-export)
|
|
63
|
+
"PLC0415", # imports inside test functions (inline json imports)
|
|
64
|
+
"PLC1901", # empty string comparisons in assertions
|
|
65
|
+
"PLC2701", # private module imports (expected in tests)
|
|
66
|
+
"PLR0913", # too many args (test helper signatures)
|
|
67
|
+
"PLR0917", # too many positional args in test calls
|
|
68
|
+
"PLR2004", # magic values in assertions
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint.isort]
|
|
72
|
+
known-first-party = ["ineepy"]
|
|
73
|
+
force-sort-within-sections = true
|
|
74
|
+
|
|
75
|
+
[tool.ruff.format]
|
|
76
|
+
preview = true
|
|
77
|
+
quote-style = "single"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""JWT auth: acquire and persist scoped access tokens via get_token()."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from json import JSONDecodeError
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ineepy._config import CONFIG_FILENAME, load_settings, write_config
|
|
10
|
+
from ineepy._exceptions import ForbiddenError, ValidationError
|
|
11
|
+
|
|
12
|
+
_AUTH_MAP: dict[int, type[ValidationError | ForbiddenError]] = {
|
|
13
|
+
400: ValidationError,
|
|
14
|
+
401: ValidationError,
|
|
15
|
+
403: ForbiddenError,
|
|
16
|
+
422: ValidationError,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _raise_auth(response: httpx.Response) -> None:
|
|
21
|
+
if response.is_error:
|
|
22
|
+
exc_class = _AUTH_MAP.get(response.status_code, ValidationError)
|
|
23
|
+
try:
|
|
24
|
+
detail = response.json().get('detail', '')
|
|
25
|
+
except Exception:
|
|
26
|
+
detail = ''
|
|
27
|
+
raise exc_class(response.status_code, detail)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_token( # noqa: PLR0913
|
|
31
|
+
email: str,
|
|
32
|
+
password: str,
|
|
33
|
+
scopes: Sequence[str] = ['user'],
|
|
34
|
+
*,
|
|
35
|
+
base_url: str | None = None,
|
|
36
|
+
api_path: str | None = None,
|
|
37
|
+
config_file: Path | None = None,
|
|
38
|
+
save_token: bool = True,
|
|
39
|
+
) -> str | None:
|
|
40
|
+
"""Obtain a JWT access token via email/password credentials.
|
|
41
|
+
|
|
42
|
+
Configuration is resolved from three sources in priority order
|
|
43
|
+
(highest to lowest): code-level arguments → ``ineepy.toml`` in the
|
|
44
|
+
current directory → ``INEEPY_*`` environment variables.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
email: User email address.
|
|
48
|
+
password: User password.
|
|
49
|
+
scopes: Requested scopes. Defaults to ``['user']``.
|
|
50
|
+
base_url: API base URL. Defaults to config/env ``INEEPY_BASE_URL``
|
|
51
|
+
(default ``https://ineep-lakehouse-prod.exe.xyz``).
|
|
52
|
+
api_path: API path prefix. Defaults to config/env ``INEEPY_API_PATH``
|
|
53
|
+
(default ``/api/v0``).
|
|
54
|
+
config_file: Path to the TOML config file. Defaults to
|
|
55
|
+
``ineepy.toml`` in the current working directory.
|
|
56
|
+
save_token: When ``True`` (default), writes ``base_url``,
|
|
57
|
+
``api_path``, and ``token`` to the config file and returns
|
|
58
|
+
``None``. When ``False``, skips writing and returns the token
|
|
59
|
+
string.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
``None`` when saved to config (``save_token=True``);
|
|
63
|
+
the ``access_token`` string otherwise.
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
cfg_path = config_file or Path.cwd() / CONFIG_FILENAME
|
|
67
|
+
s = load_settings(cfg_path)
|
|
68
|
+
eff_base_url = base_url or s.base_url
|
|
69
|
+
eff_api_path = api_path or s.api_path
|
|
70
|
+
url = f'{eff_base_url}{eff_api_path}/auth/token'
|
|
71
|
+
_scopes = scopes if scopes is not None else ['user']
|
|
72
|
+
data = {
|
|
73
|
+
'username': email,
|
|
74
|
+
'password': password,
|
|
75
|
+
'scope': ' '.join(_scopes),
|
|
76
|
+
}
|
|
77
|
+
with httpx.Client() as client:
|
|
78
|
+
response = client.post(url, data=data)
|
|
79
|
+
_raise_auth(response)
|
|
80
|
+
try:
|
|
81
|
+
token = response.json()['access_token']
|
|
82
|
+
except JSONDecodeError, KeyError:
|
|
83
|
+
raise ValidationError(
|
|
84
|
+
response.status_code, 'Failed to extract access token'
|
|
85
|
+
)
|
|
86
|
+
if save_token:
|
|
87
|
+
write_config(
|
|
88
|
+
cfg_path,
|
|
89
|
+
{
|
|
90
|
+
'base_url': eff_base_url,
|
|
91
|
+
'api_path': eff_api_path,
|
|
92
|
+
'token': token,
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
return None
|
|
96
|
+
return token
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def async_get_token( # noqa: PLR0913
|
|
100
|
+
email: str,
|
|
101
|
+
password: str,
|
|
102
|
+
scopes: Sequence[str] = ['user'],
|
|
103
|
+
*,
|
|
104
|
+
base_url: str | None = None,
|
|
105
|
+
api_path: str | None = None,
|
|
106
|
+
config_file: Path | None = None,
|
|
107
|
+
save_token: bool = True,
|
|
108
|
+
) -> str | None:
|
|
109
|
+
"""Async version of :func:`get_token`."""
|
|
110
|
+
cfg_path = config_file or Path.cwd() / CONFIG_FILENAME
|
|
111
|
+
s = load_settings(cfg_path)
|
|
112
|
+
eff_base_url = base_url or s.base_url
|
|
113
|
+
eff_api_path = api_path or s.api_path
|
|
114
|
+
url = f'{eff_base_url}{eff_api_path}/auth/token'
|
|
115
|
+
_scopes = scopes if scopes is not None else ['user']
|
|
116
|
+
data = {
|
|
117
|
+
'username': email,
|
|
118
|
+
'password': password,
|
|
119
|
+
'scope': ' '.join(_scopes),
|
|
120
|
+
}
|
|
121
|
+
async with httpx.AsyncClient() as client:
|
|
122
|
+
response = await client.post(url, data=data)
|
|
123
|
+
_raise_auth(response)
|
|
124
|
+
try:
|
|
125
|
+
token = response.json()['access_token']
|
|
126
|
+
except JSONDecodeError, KeyError:
|
|
127
|
+
raise ValidationError(
|
|
128
|
+
response.status_code, 'Failed to extract access token'
|
|
129
|
+
)
|
|
130
|
+
if save_token:
|
|
131
|
+
write_config(
|
|
132
|
+
cfg_path,
|
|
133
|
+
{
|
|
134
|
+
'base_url': eff_base_url,
|
|
135
|
+
'api_path': eff_api_path,
|
|
136
|
+
'token': token,
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
return None
|
|
140
|
+
return token
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async_get_token.__doc__ = get_token.__doc__
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Sync and async client (Ineepy, AsyncIneepy) for the ineep lakehouse."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ineepy._config import CONFIG_FILENAME, load_settings
|
|
11
|
+
from ineepy._resources.api_keys import APIKeysResource, AsyncAPIKeysResource
|
|
12
|
+
from ineepy._resources.data import AsyncDataResource, DataResource
|
|
13
|
+
from ineepy._resources.namespaces import (
|
|
14
|
+
AsyncNamespacesResource,
|
|
15
|
+
NamespacesResource,
|
|
16
|
+
)
|
|
17
|
+
from ineepy._resources.tables import AsyncTablesResource, TablesResource
|
|
18
|
+
from ineepy._resources.users import AsyncUsersResource, UsersResource
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Ineepy:
|
|
22
|
+
"""Synchronous client for the **ineep lakehouse API**.
|
|
23
|
+
|
|
24
|
+
Configuration is resolved from three sources in priority order
|
|
25
|
+
(highest to lowest): code-level arguments → ``ineepy.toml`` in the
|
|
26
|
+
current directory → ``INEEPY_*`` environment variables.
|
|
27
|
+
|
|
28
|
+
Can be used as a context manager (preferred) or manually closed.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
token: Bearer token. Defaults to config/env ``INEEPY_TOKEN``.
|
|
32
|
+
Obtain one with :func:`~ineepy.get_token`.
|
|
33
|
+
base_url: API base URL. Defaults to config/env ``INEEPY_BASE_URL``
|
|
34
|
+
(default ``https://ineep-lakehouse-prod.exe.xyz``).
|
|
35
|
+
api_path: API path prefix. Defaults to config/env ``INEEPY_API_PATH``
|
|
36
|
+
(default ``/api/v0``).
|
|
37
|
+
config_file: Path to the TOML config file. Defaults to
|
|
38
|
+
``ineepy.toml`` in the current working directory.
|
|
39
|
+
|
|
40
|
+
Example::
|
|
41
|
+
|
|
42
|
+
# After calling get_token(...) once, the token is saved to
|
|
43
|
+
# ineepy.toml and the client loads it automatically:
|
|
44
|
+
with Ineepy() as client:
|
|
45
|
+
user, etag = client.users.get('me')
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
token: str | None = None,
|
|
51
|
+
*,
|
|
52
|
+
base_url: str | None = None,
|
|
53
|
+
api_path: str | None = None,
|
|
54
|
+
config_file: Path | None = None,
|
|
55
|
+
_skip_health_check: bool = False,
|
|
56
|
+
) -> None:
|
|
57
|
+
s = load_settings(config_file or Path.cwd() / CONFIG_FILENAME)
|
|
58
|
+
_base = (base_url or s.base_url).rstrip('/')
|
|
59
|
+
_path = (api_path or s.api_path).rstrip('/')
|
|
60
|
+
self._health_url = f'{_base}/health'
|
|
61
|
+
self._http = httpx.Client(
|
|
62
|
+
base_url=f'{_base}{_path}/',
|
|
63
|
+
headers={'Authorization': f'Bearer {token or s.token}'},
|
|
64
|
+
)
|
|
65
|
+
self.users = UsersResource(self._http)
|
|
66
|
+
self.api_keys = APIKeysResource(self._http)
|
|
67
|
+
self.namespaces = NamespacesResource(self._http)
|
|
68
|
+
self.tables = TablesResource(self._http)
|
|
69
|
+
self.data = DataResource(self._http)
|
|
70
|
+
if not _skip_health_check:
|
|
71
|
+
self._check_health()
|
|
72
|
+
|
|
73
|
+
def _check_health(self) -> None:
|
|
74
|
+
r = self._http.get(self._health_url)
|
|
75
|
+
r.raise_for_status()
|
|
76
|
+
|
|
77
|
+
def close(self) -> None:
|
|
78
|
+
"""Close the underlying HTTP client."""
|
|
79
|
+
self._http.close()
|
|
80
|
+
|
|
81
|
+
def __enter__(self) -> Ineepy:
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def __exit__(
|
|
85
|
+
self,
|
|
86
|
+
exc_type: type[BaseException] | None,
|
|
87
|
+
exc_val: BaseException | None,
|
|
88
|
+
exc_tb: TracebackType | None,
|
|
89
|
+
) -> None:
|
|
90
|
+
self.close()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AsyncIneepy:
|
|
94
|
+
"""Async version of :class:`~ineepy.Ineepy`."""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
token: str | None = None,
|
|
99
|
+
*,
|
|
100
|
+
base_url: str | None = None,
|
|
101
|
+
api_path: str | None = None,
|
|
102
|
+
config_file: Path | None = None,
|
|
103
|
+
_skip_health_check: bool = False,
|
|
104
|
+
) -> None:
|
|
105
|
+
s = load_settings(config_file or Path.cwd() / CONFIG_FILENAME)
|
|
106
|
+
_base = (base_url or s.base_url).rstrip('/')
|
|
107
|
+
_path = (api_path or s.api_path).rstrip('/')
|
|
108
|
+
self._health_url = f'{_base}/health'
|
|
109
|
+
self._http = httpx.AsyncClient(
|
|
110
|
+
base_url=f'{_base}{_path}/',
|
|
111
|
+
headers={'Authorization': f'Bearer {token or s.token}'},
|
|
112
|
+
)
|
|
113
|
+
self.users = AsyncUsersResource(self._http)
|
|
114
|
+
self.api_keys = AsyncAPIKeysResource(self._http)
|
|
115
|
+
self.namespaces = AsyncNamespacesResource(self._http)
|
|
116
|
+
self.tables = AsyncTablesResource(self._http)
|
|
117
|
+
self.data = AsyncDataResource(self._http)
|
|
118
|
+
self._skip_health_check = _skip_health_check
|
|
119
|
+
|
|
120
|
+
async def aclose(self) -> None:
|
|
121
|
+
"""Close the underlying async HTTP client."""
|
|
122
|
+
await self._http.aclose()
|
|
123
|
+
|
|
124
|
+
async def _acheck_health(self) -> None:
|
|
125
|
+
r = await self._http.get(self._health_url)
|
|
126
|
+
r.raise_for_status()
|
|
127
|
+
|
|
128
|
+
async def __aenter__(self) -> AsyncIneepy:
|
|
129
|
+
if not self._skip_health_check:
|
|
130
|
+
await self._acheck_health()
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
async def __aexit__(
|
|
134
|
+
self,
|
|
135
|
+
exc_type: type[BaseException] | None,
|
|
136
|
+
exc_val: BaseException | None,
|
|
137
|
+
exc_tb: TracebackType | None,
|
|
138
|
+
) -> None:
|
|
139
|
+
await self.aclose()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
AsyncIneepy.__doc__ = Ineepy.__doc__
|
|
143
|
+
AsyncIneepy.aclose.__doc__ = Ineepy.close.__doc__
|