kamori-sdk 0.1.1__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,31 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ci-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ name: Test
16
+ runs-on: ubuntu-latest
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.11"
24
+ cache: "pip"
25
+ cache-dependency-path: pyproject.toml
26
+
27
+ - name: Install package
28
+ run: pip install -e ".[dev]"
29
+
30
+ - name: Run tests
31
+ run: python -m pytest
@@ -0,0 +1,50 @@
1
+ name: PR Checks
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ types: [opened, synchronize, reopened, edited]
7
+
8
+ concurrency:
9
+ group: pr-${{ github.event.pull_request.number }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ validate-title:
14
+ name: Validate PR title
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: amannn/action-semantic-pull-request@v5
18
+ env:
19
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20
+ with:
21
+ types: |
22
+ feat
23
+ fix
24
+ chore
25
+ docs
26
+ refactor
27
+ test
28
+ ci
29
+ perf
30
+ revert
31
+ build
32
+ requireScope: false
33
+
34
+ test:
35
+ name: Test
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+
40
+ - uses: actions/setup-python@v5
41
+ with:
42
+ python-version: "3.11"
43
+ cache: "pip"
44
+ cache-dependency-path: pyproject.toml
45
+
46
+ - name: Install package
47
+ run: pip install -e ".[dev]"
48
+
49
+ - name: Run tests
50
+ run: python -m pytest
@@ -0,0 +1,101 @@
1
+ name: Release
2
+
3
+ # Triggered by pushing a semver tag (e.g. git tag v1.1.0 && git push --tags).
4
+ # The version in pyproject.toml must be bumped and committed before tagging
5
+ # (scripts/release.sh handles this automatically).
6
+ #
7
+ # PyPI authentication — OIDC Trusted Publishers (no PYPI_TOKEN secret needed).
8
+ # One-time setup on pypi.org:
9
+ # Account → Publishing → Add a new pending publisher
10
+ # → owner: usekamori, repo: kamori-python, workflow: release.yml, environment: pypi
11
+ #
12
+ # Pipeline:
13
+ # test → install + pytest (blocks publish if failing)
14
+ # publish-pypi → build + push to PyPI via OIDC
15
+ # github-release → GitHub Release with auto-generated changelog
16
+
17
+ on:
18
+ push:
19
+ tags:
20
+ - "v*"
21
+
22
+ permissions:
23
+ contents: write
24
+ id-token: write # OIDC for PyPI provenance
25
+
26
+ concurrency:
27
+ group: release-${{ github.ref }}
28
+ cancel-in-progress: false
29
+
30
+ jobs:
31
+ test:
32
+ name: Test
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+
37
+ - uses: actions/setup-python@v5
38
+ with:
39
+ python-version: "3.11"
40
+
41
+ - name: Install package
42
+ run: pip install -e ".[dev]"
43
+
44
+ - name: Run tests
45
+ run: python -m pytest
46
+
47
+ publish-pypi:
48
+ name: Publish to PyPI
49
+ runs-on: ubuntu-latest
50
+ needs: test
51
+ environment: pypi
52
+
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+
56
+ - uses: actions/setup-python@v5
57
+ with:
58
+ python-version: "3.11"
59
+
60
+ - name: Install build tools
61
+ run: pip install build
62
+
63
+ - name: Build distribution
64
+ run: python -m build
65
+
66
+ - name: Publish to PyPI
67
+ uses: pypa/gh-action-pypi-publish@release/v1
68
+
69
+ github-release:
70
+ name: Create GitHub Release
71
+ runs-on: ubuntu-latest
72
+ needs: publish-pypi
73
+ steps:
74
+ - uses: actions/checkout@v4
75
+ with:
76
+ fetch-depth: 0
77
+
78
+ - name: Create release
79
+ uses: softprops/action-gh-release@v2
80
+ with:
81
+ generate_release_notes: true
82
+ prerelease: ${{ contains(github.ref_name, '-') }}
83
+ body: |
84
+ ## Install
85
+
86
+ ```bash
87
+ pip install kamori-sdk==${{ github.ref_name }}
88
+ ```
89
+
90
+ ## Usage
91
+
92
+ ```python
93
+ from kamori_sdk import KamoriClient
94
+
95
+ client = KamoriClient(
96
+ url="https://your-kamori-server.com",
97
+ token="your-ingest-token",
98
+ )
99
+ client.log({"level": "info", "message": "hello"})
100
+ client.flush()
101
+ ```
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ .pytest_cache/
6
+ dist/
7
+ *.egg-info/
8
+ .DS_Store
@@ -0,0 +1,98 @@
1
+ Elastic License 2.0
2
+
3
+ URL: https://www.elastic.co/licensing/elastic-license
4
+
5
+ ## Acceptance
6
+
7
+ By using the software, you agree to all of the terms and conditions below.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
12
+ non-sublicensable, non-transferable license to use, copy, distribute, make
13
+ available, and prepare derivative works of the software, in each case subject
14
+ to the limitations and conditions below.
15
+
16
+ ## Limitations
17
+
18
+ **You may not provide the software to third parties as a hosted or managed
19
+ service**, where the service provides users with access to any substantial set
20
+ of the features or functionality of the software.
21
+
22
+ You may not move, change, disable, or circumvent the license key functionality
23
+ in the software, and you may not remove or obscure any functionality in the
24
+ software that is protected by the license key.
25
+
26
+ You may not alter, remove, or obscure any licensing, copyright, or other
27
+ notices of the licensor in the software. Any use of the licensor's trademarks
28
+ is subject to applicable law.
29
+
30
+ ## Patents
31
+
32
+ The licensor grants you a license, under any patent claims the licensor can
33
+ license, or chooses to license, to make, have made, use, sell, offer for sale,
34
+ import and have imported the software, in each case subject to the limitations
35
+ and conditions in this license. This license does not cover any patent claims
36
+ that you cause to be infringed by modifications or additions to the software.
37
+ If you or your company make any written claim that the software infringes or
38
+ contributes to infringement of any patent, your patent license for the software
39
+ granted under these terms ends immediately. If your company makes such a claim,
40
+ your patent license ends immediately for work on behalf of your company.
41
+
42
+ ## Notices
43
+
44
+ You must ensure that anyone who gets a copy of any part of the software from
45
+ you also gets a copy of these terms.
46
+
47
+ If you modify the software, you must include in any modified copies of the
48
+ software prominent notices stating that you have modified the software.
49
+
50
+ ## No Other Rights
51
+
52
+ These terms do not imply any licenses other than those expressly granted in
53
+ these terms.
54
+
55
+ ## Termination
56
+
57
+ If you use the software in violation of these terms, such use is not licensed,
58
+ and your licenses will automatically terminate. If the licensor provides you
59
+ with a notice of your violation, and you cease all violation of this license no
60
+ later than 30 days after you receive that notice, your licenses will be
61
+ reinstated retroactively. However, if you violate these terms after such
62
+ reinstatement, any additional violation of these terms will cause your licenses
63
+ to terminate automatically and permanently.
64
+
65
+ ## No Liability
66
+
67
+ _As far as the law allows, the software comes as is, without any warranty or
68
+ condition, and the licensor will not be liable to you for any damages arising
69
+ out of these terms or the use or nature of the software, under any kind of
70
+ legal claim._
71
+
72
+ ## Definitions
73
+
74
+ The **licensor** is the entity offering these terms, and the **software** is
75
+ the software the licensor makes available under these terms, including any
76
+ portion of it.
77
+
78
+ **you** refers to the individual or entity agreeing to these terms.
79
+
80
+ **your company** is any legal entity, sole proprietorship, or other kind of
81
+ organization that you work for, plus all organizations that have control over,
82
+ are under the control of, or are under common control with that organization.
83
+ **control** means ownership of substantially all the assets of an entity, or
84
+ the power to direct its management and legal affairs.
85
+
86
+ **your licenses** are all the licenses granted to you for the software under
87
+ these terms.
88
+
89
+ **use** includes anything you do with the software requiring one of your
90
+ licenses.
91
+
92
+ **trademark** means trademarks, service marks, and similar rights.
93
+
94
+ ---
95
+
96
+ Note: The Kamori client SDKs (@kamori/sdk, kamori-go, kamori-sdk, kamori-php)
97
+ are separately licensed under the MIT License to allow unrestricted use in
98
+ any application.
@@ -0,0 +1,264 @@
1
+ Metadata-Version: 2.4
2
+ Name: kamori-sdk
3
+ Version: 0.1.1
4
+ Summary: Python SDK for Kamori — self-hosted log ingestion
5
+ Project-URL: Homepage, https://github.com/usekamori/kamori-python
6
+ License: MIT
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Topic :: System :: Logging
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.9
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=7.0; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # kamori-sdk
21
+
22
+ Python SDK for [Kamori](https://github.com/usekamori/kamori) — self-hosted log ingestion with MCP support.
23
+
24
+ Events are buffered in memory and flushed to your Kamori server on a background daemon thread, so logging never blocks your application.
25
+
26
+ ---
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install kamori-sdk
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ import atexit
40
+ from kamori_sdk import KamoriClient
41
+
42
+ client = KamoriClient(
43
+ url="https://your-kamori-server.com",
44
+ token="your-log-token", # matches INGEST_TOKEN on the server
45
+ )
46
+ atexit.register(client.shutdown) # flush buffered events on process exit
47
+
48
+ client.log({"level": "info", "service": "api", "message": "Server started"})
49
+ client.log({"level": "error", "service": "payments", "message": "Stripe timeout", "duration_ms": 5001})
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Python `logging` integration
55
+
56
+ ### One-liner (recommended)
57
+
58
+ ```python
59
+ from kamori_sdk import install_logging_handler
60
+
61
+ handler = install_logging_handler(
62
+ url="https://your-kamori-server.com",
63
+ token="your-log-token",
64
+ )
65
+
66
+ import logging
67
+ logging.getLogger("api").info("Server started")
68
+ logging.getLogger("payments").error("Stripe timeout")
69
+ ```
70
+
71
+ Each `LogRecord` is converted to a structured dict with `level`, `message`, `logger`, `module`, `funcName`, and `lineno` fields. Exception info is serialised as `exc_text` so stack traces are full-text-searchable in Kamori.
72
+
73
+ ### Explicit handler
74
+
75
+ ```python
76
+ import logging
77
+ from kamori_sdk.logging_handler import KamoriHandler
78
+
79
+ handler = KamoriHandler(
80
+ url="https://your-kamori-server.com",
81
+ token="your-log-token",
82
+ level=logging.WARNING, # only WARNING and above
83
+ )
84
+ logging.getLogger().addHandler(handler)
85
+ ```
86
+
87
+ ### Django
88
+
89
+ In `settings.py`:
90
+
91
+ ```python
92
+ LOGGING = {
93
+ "version": 1,
94
+ "disable_existing_loggers": False,
95
+ "handlers": {
96
+ "kamori": {
97
+ "class": "kamori_sdk.logging_handler.KamoriHandler",
98
+ "url": "https://your-kamori-server.com",
99
+ "token": env("INGEST_TOKEN", default=""),
100
+ },
101
+ },
102
+ "root": {"handlers": ["kamori"], "level": "INFO"},
103
+ }
104
+ ```
105
+
106
+ ### FastAPI
107
+
108
+ ```python
109
+ from contextlib import asynccontextmanager
110
+ from fastapi import FastAPI
111
+ from kamori_sdk import install_logging_handler
112
+
113
+ @asynccontextmanager
114
+ async def lifespan(app: FastAPI):
115
+ handler = install_logging_handler(
116
+ url=settings.KAMORI_URL,
117
+ token=settings.INGEST_TOKEN,
118
+ )
119
+ yield
120
+ handler.client.shutdown() # flush before shutdown
121
+
122
+ app = FastAPI(lifespan=lifespan)
123
+ ```
124
+
125
+ ### structlog
126
+
127
+ ```python
128
+ import structlog
129
+ from kamori_sdk import KamoriClient
130
+
131
+ client = KamoriClient(url="https://your-kamori-server.com", token="your-log-token")
132
+
133
+ def kamori_sink(logger, method, event_dict):
134
+ client.log(event_dict)
135
+ return event_dict
136
+
137
+ structlog.configure(
138
+ processors=[
139
+ structlog.processors.add_log_level,
140
+ structlog.processors.TimeStamper(fmt="iso"),
141
+ kamori_sink,
142
+ structlog.dev.ConsoleRenderer(),
143
+ ]
144
+ )
145
+ ```
146
+
147
+ ### loguru
148
+
149
+ ```python
150
+ from loguru import logger
151
+ from kamori_sdk import KamoriClient
152
+
153
+ client = KamoriClient(url="https://your-kamori-server.com", token="your-log-token")
154
+
155
+ def kamori_sink(message):
156
+ record = message.record
157
+ client.log({
158
+ "level": record["level"].name.lower(),
159
+ "message": record["message"],
160
+ "module": record["module"],
161
+ "function": record["function"],
162
+ "line": record["line"],
163
+ })
164
+
165
+ logger.add(kamori_sink)
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Scoped clients
171
+
172
+ Add default fields to every log call without repeating them:
173
+
174
+ ```python
175
+ def make_scoped(client: KamoriClient, **defaults):
176
+ """Return a log function that merges defaults into every event."""
177
+ def log(event):
178
+ client.log({**defaults, **event})
179
+ return log
180
+
181
+ api_log = make_scoped(client, service="api", version="2.1.0")
182
+ api_log({"level": "info", "message": "Request started", "path": "/checkout"})
183
+ api_log({"level": "error", "message": "DB timeout"})
184
+ ```
185
+
186
+ ---
187
+
188
+ ## flush_on_exit
189
+
190
+ The background thread is a **daemon thread** — it dies automatically when the main thread exits. For guaranteed delivery, register `shutdown()` via `atexit`:
191
+
192
+ ```python
193
+ import atexit
194
+ atexit.register(client.shutdown) # flush + stop background thread (up to 5s)
195
+ ```
196
+
197
+ Or call explicitly:
198
+
199
+ ```python
200
+ client.shutdown(timeout=5.0) # blocks up to 5 seconds
201
+ ```
202
+
203
+ For an immediate flush without stopping the thread (useful in request handlers):
204
+
205
+ ```python
206
+ client.flush() # blocks until the queue drains
207
+ ```
208
+
209
+ ---
210
+
211
+ ## on_drop callback
212
+
213
+ Called when a batch is permanently dropped after all retry attempts:
214
+
215
+ ```python
216
+ import logging
217
+
218
+ def handle_drop(events):
219
+ logging.warning("Kamori dropped %d events — check server connectivity", len(events))
220
+
221
+ client = KamoriClient(
222
+ url="https://your-kamori-server.com",
223
+ token="your-log-token",
224
+ on_drop=handle_drop,
225
+ )
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Retry behaviour
231
+
232
+ Failed requests are retried up to three times with exponential back-off:
233
+
234
+ | Attempt | Delay |
235
+ | --------- | ------ |
236
+ | 1st retry | 0.25 s |
237
+ | 2nd retry | 1 s |
238
+ | 3rd retry | 4 s |
239
+
240
+ `4xx` responses are **not** retried (client error — bad token, oversized batch). The client never raises or crashes the calling thread.
241
+
242
+ ---
243
+
244
+ ## Configuration reference
245
+
246
+ | Parameter | Default | Description |
247
+ | ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
248
+ | `url` | — | Base URL of your Kamori server (required) |
249
+ | `token` | `None` | Auth token (sent as `Authorization: Bearer`). Omit to skip auth. |
250
+ | `batch_size` | `50` | Flush automatically when buffer reaches this size |
251
+ | `flush_interval` | `2.0` | Max seconds between background flushes |
252
+ | `max_queue` | `0` | Max events in the in-memory queue. `0` = unlimited. New events are dropped (calling `on_drop`) when the limit is reached. |
253
+ | `on_drop` | `None` | `Callable[[list[dict]], None]` — called when a batch is dropped |
254
+
255
+ ---
256
+
257
+ ## Requirements
258
+
259
+ - Python 3.9+
260
+ - No external dependencies (uses `urllib.request` and `threading` from the standard library)
261
+
262
+ ## License
263
+
264
+ MIT