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 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
+ [![status-badge](https://woodpecker-ineep.exe.xyz:8000/api/badges/4/status.svg)](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
+ [![status-badge](https://woodpecker-ineep.exe.xyz:8000/api/badges/4/status.svg)](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,4 @@
1
+ from ineepy._auth import async_get_token, get_token
2
+ from ineepy._client import AsyncIneepy, Ineepy
3
+
4
+ __all__ = ['Ineepy', 'AsyncIneepy', 'get_token', 'async_get_token']
@@ -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__