tinyshed-tinystore 0.1.0rc2__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.
- tinyshed_tinystore-0.1.0rc2/PKG-INFO +135 -0
- tinyshed_tinystore-0.1.0rc2/README.md +120 -0
- tinyshed_tinystore-0.1.0rc2/pyproject.toml +99 -0
- tinyshed_tinystore-0.1.0rc2/pyproject.toml.orig +78 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/__init__.py +111 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_background.py +107 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_cli.py +42 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_connection.py +446 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_console.py +310 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_page.py +16 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_runtime.py +177 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_session.py +503 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_time.py +48 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_trace.py +11 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_values.py +113 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_wire/__init__.py +1 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_wire/codec.py +305 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_wire/frame.py +107 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_wire/messages.py +535 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/_wire/msgpack.py +483 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/blobs.py +345 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/clock.py +47 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/config.py +467 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/errors.py +175 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/jobs.py +742 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/kv.py +791 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/limiter.py +73 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/metrics.py +719 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/py.typed +0 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/quota.py +104 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/records.py +574 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/sql.py +321 -0
- tinyshed_tinystore-0.1.0rc2/src/tinystore/store.py +176 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinyshed-tinystore
|
|
3
|
+
Version: 0.1.0rc2
|
|
4
|
+
Summary: TinyStore for Python: kv, jobs, blobs, SQL, records and metrics in one directory, served by a sidecar
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Framework :: AsyncIO
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Project-URL: Repository, https://github.com/tinyshed/tinystore
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
<!--
|
|
17
|
+
This is the page PyPI shows. The headline, the sample and the engines are the
|
|
18
|
+
landing page's: task readme writes them between the landing: markers from
|
|
19
|
+
web/landing.md. Every link is absolute, because PyPI cannot follow a relative
|
|
20
|
+
one. Every call, by engine, is in docs/reference/python.md.
|
|
21
|
+
-->
|
|
22
|
+
|
|
23
|
+
# TinyStore for Python
|
|
24
|
+
|
|
25
|
+
<!-- landing:headline -->
|
|
26
|
+
|
|
27
|
+
**A small storage runtime for applications.** SQL, key-value state, durable jobs, files, metrics and logs in one directory, with one lifecycle, one memory budget and one backup.
|
|
28
|
+
|
|
29
|
+
<!-- /landing:headline -->
|
|
30
|
+
|
|
31
|
+
> **Release candidates first.** Until `v0.1.0`, the API may still change, and
|
|
32
|
+
> a file written by one release may not open in the next.
|
|
33
|
+
|
|
34
|
+
The SDK starts a small `tinystore` server next to your program, called the
|
|
35
|
+
sidecar, and talks to it over a local connection. The server binary comes with the
|
|
36
|
+
wheel, so there is nothing else to install. Go programs embed the same
|
|
37
|
+
engines, and every client of one directory sees the same data.
|
|
38
|
+
|
|
39
|
+
<!-- landing:sample -->
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import asyncio
|
|
43
|
+
import logging
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
|
|
46
|
+
import tinystore
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def main() -> None:
|
|
50
|
+
async with tinystore.open("./data") as store:
|
|
51
|
+
db = await store.sql("app")
|
|
52
|
+
await db.exec("insert into users (id, name) values (?, ?)", 42, "Ada")
|
|
53
|
+
|
|
54
|
+
sessions = store.kv.bucket("sessions", str, sliding="30d")
|
|
55
|
+
await sessions.of("42").set("token", "abc123")
|
|
56
|
+
|
|
57
|
+
emails = store.jobs.queue("emails", dict[str, str])
|
|
58
|
+
await emails.enqueue({"to": "ada@example.com"})
|
|
59
|
+
|
|
60
|
+
files = store.blobs.bucket("files")
|
|
61
|
+
await files.put("avatars/42.png", Path("avatar.png").read_bytes())
|
|
62
|
+
|
|
63
|
+
logging.getLogger().addHandler(store.records.handler("api"))
|
|
64
|
+
logging.info("user created", extra={"user_id": 42})
|
|
65
|
+
|
|
66
|
+
store.metrics.counter("signups_total").inc()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
pip install tinyshed-tinystore
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
<!-- /landing:sample -->
|
|
77
|
+
|
|
78
|
+
It needs Python 3.12 or later and runs on asyncio. The package installs as
|
|
79
|
+
`tinyshed-tinystore` and imports as `tinystore`.
|
|
80
|
+
|
|
81
|
+
## Engines
|
|
82
|
+
|
|
83
|
+
<!-- landing:engines -->
|
|
84
|
+
|
|
85
|
+
| | | |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| [SQL](https://github.com/tinyshed/tinystore/blob/main/docs/sql/README.md) | Relational state | The application's own SQL databases: tables from structs, checked migrations. |
|
|
88
|
+
| [KV](https://github.com/tinyshed/tinystore/blob/main/docs/kv/README.md) | Application state | Current state: typed buckets, counters, expiry, versions. |
|
|
89
|
+
| [Jobs](https://github.com/tinyshed/tinystore/blob/main/docs/jobs/README.md) | Durable background work | Work that runs at its time: retries, leases, repeats. |
|
|
90
|
+
| [Blobs](https://github.com/tinyshed/tinystore/blob/main/docs/blobs/README.md) | Files and objects | Files by path, checked when read whole. |
|
|
91
|
+
| [Records](https://github.com/tinyshed/tinystore/blob/main/docs/records/README.md) | Logs and events | Read by time, level and keys. |
|
|
92
|
+
| [Metrics](https://github.com/tinyshed/tinystore/blob/main/docs/metrics/README.md) | Time series | Samples kept bit for bit, answered exactly. |
|
|
93
|
+
|
|
94
|
+
<!-- /landing:engines -->
|
|
95
|
+
|
|
96
|
+
## Three ways to connect
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
async with tinystore.open("./data") as store: # the directory's sidecar, shared with other processes
|
|
100
|
+
...
|
|
101
|
+
async with tinystore.open("./data", private=True) as store: # a server for this process alone, for tests and scripts
|
|
102
|
+
...
|
|
103
|
+
async with tinystore.connect("tls://db.internal:7070", token=token) as store:
|
|
104
|
+
...
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
[The sidecar](https://github.com/tinyshed/tinystore/blob/main/docs/running/sidecar.md)
|
|
108
|
+
and [A remote server](https://github.com/tinyshed/tinystore/blob/main/docs/running/server.md)
|
|
109
|
+
explain each one.
|
|
110
|
+
|
|
111
|
+
## The command line
|
|
112
|
+
|
|
113
|
+
The wheel also installs the `tinystore` command. It shows what a store
|
|
114
|
+
contains and follows its logs while your program runs. `uvx` runs it without
|
|
115
|
+
installing anything:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
uvx --from tinyshed-tinystore tinystore status ./data # each engine's size, and who serves the directory
|
|
119
|
+
tinystore logs ./data -f # the application's logs, as they arrive
|
|
120
|
+
claude mcp add tinystore -- uvx --from tinyshed-tinystore tinystore mcp ./data # read-only access for an AI agent
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
See [The command line](https://github.com/tinyshed/tinystore/blob/main/docs/running/cli.md)
|
|
124
|
+
for every command.
|
|
125
|
+
|
|
126
|
+
## Documentation
|
|
127
|
+
|
|
128
|
+
- [Getting started](https://github.com/tinyshed/tinystore/blob/main/docs/getting-started.md): install it and write a first program
|
|
129
|
+
- [A tour](https://github.com/tinyshed/tinystore/blob/main/docs/tour.md): every engine on one page
|
|
130
|
+
- [The guides](https://github.com/tinyshed/tinystore/blob/main/docs/README.md): a page per feature, each example in Bun, Python and Go
|
|
131
|
+
- [Python API](https://github.com/tinyshed/tinystore/blob/main/docs/reference/python.md): every call, by engine
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
Apache-2.0.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
This is the page PyPI shows. The headline, the sample and the engines are the
|
|
3
|
+
landing page's: task readme writes them between the landing: markers from
|
|
4
|
+
web/landing.md. Every link is absolute, because PyPI cannot follow a relative
|
|
5
|
+
one. Every call, by engine, is in docs/reference/python.md.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# TinyStore for Python
|
|
9
|
+
|
|
10
|
+
<!-- landing:headline -->
|
|
11
|
+
|
|
12
|
+
**A small storage runtime for applications.** SQL, key-value state, durable jobs, files, metrics and logs in one directory, with one lifecycle, one memory budget and one backup.
|
|
13
|
+
|
|
14
|
+
<!-- /landing:headline -->
|
|
15
|
+
|
|
16
|
+
> **Release candidates first.** Until `v0.1.0`, the API may still change, and
|
|
17
|
+
> a file written by one release may not open in the next.
|
|
18
|
+
|
|
19
|
+
The SDK starts a small `tinystore` server next to your program, called the
|
|
20
|
+
sidecar, and talks to it over a local connection. The server binary comes with the
|
|
21
|
+
wheel, so there is nothing else to install. Go programs embed the same
|
|
22
|
+
engines, and every client of one directory sees the same data.
|
|
23
|
+
|
|
24
|
+
<!-- landing:sample -->
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import asyncio
|
|
28
|
+
import logging
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
import tinystore
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def main() -> None:
|
|
35
|
+
async with tinystore.open("./data") as store:
|
|
36
|
+
db = await store.sql("app")
|
|
37
|
+
await db.exec("insert into users (id, name) values (?, ?)", 42, "Ada")
|
|
38
|
+
|
|
39
|
+
sessions = store.kv.bucket("sessions", str, sliding="30d")
|
|
40
|
+
await sessions.of("42").set("token", "abc123")
|
|
41
|
+
|
|
42
|
+
emails = store.jobs.queue("emails", dict[str, str])
|
|
43
|
+
await emails.enqueue({"to": "ada@example.com"})
|
|
44
|
+
|
|
45
|
+
files = store.blobs.bucket("files")
|
|
46
|
+
await files.put("avatars/42.png", Path("avatar.png").read_bytes())
|
|
47
|
+
|
|
48
|
+
logging.getLogger().addHandler(store.records.handler("api"))
|
|
49
|
+
logging.info("user created", extra={"user_id": 42})
|
|
50
|
+
|
|
51
|
+
store.metrics.counter("signups_total").inc()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
pip install tinyshed-tinystore
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
<!-- /landing:sample -->
|
|
62
|
+
|
|
63
|
+
It needs Python 3.12 or later and runs on asyncio. The package installs as
|
|
64
|
+
`tinyshed-tinystore` and imports as `tinystore`.
|
|
65
|
+
|
|
66
|
+
## Engines
|
|
67
|
+
|
|
68
|
+
<!-- landing:engines -->
|
|
69
|
+
|
|
70
|
+
| | | |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| [SQL](https://github.com/tinyshed/tinystore/blob/main/docs/sql/README.md) | Relational state | The application's own SQL databases: tables from structs, checked migrations. |
|
|
73
|
+
| [KV](https://github.com/tinyshed/tinystore/blob/main/docs/kv/README.md) | Application state | Current state: typed buckets, counters, expiry, versions. |
|
|
74
|
+
| [Jobs](https://github.com/tinyshed/tinystore/blob/main/docs/jobs/README.md) | Durable background work | Work that runs at its time: retries, leases, repeats. |
|
|
75
|
+
| [Blobs](https://github.com/tinyshed/tinystore/blob/main/docs/blobs/README.md) | Files and objects | Files by path, checked when read whole. |
|
|
76
|
+
| [Records](https://github.com/tinyshed/tinystore/blob/main/docs/records/README.md) | Logs and events | Read by time, level and keys. |
|
|
77
|
+
| [Metrics](https://github.com/tinyshed/tinystore/blob/main/docs/metrics/README.md) | Time series | Samples kept bit for bit, answered exactly. |
|
|
78
|
+
|
|
79
|
+
<!-- /landing:engines -->
|
|
80
|
+
|
|
81
|
+
## Three ways to connect
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
async with tinystore.open("./data") as store: # the directory's sidecar, shared with other processes
|
|
85
|
+
...
|
|
86
|
+
async with tinystore.open("./data", private=True) as store: # a server for this process alone, for tests and scripts
|
|
87
|
+
...
|
|
88
|
+
async with tinystore.connect("tls://db.internal:7070", token=token) as store:
|
|
89
|
+
...
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
[The sidecar](https://github.com/tinyshed/tinystore/blob/main/docs/running/sidecar.md)
|
|
93
|
+
and [A remote server](https://github.com/tinyshed/tinystore/blob/main/docs/running/server.md)
|
|
94
|
+
explain each one.
|
|
95
|
+
|
|
96
|
+
## The command line
|
|
97
|
+
|
|
98
|
+
The wheel also installs the `tinystore` command. It shows what a store
|
|
99
|
+
contains and follows its logs while your program runs. `uvx` runs it without
|
|
100
|
+
installing anything:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
uvx --from tinyshed-tinystore tinystore status ./data # each engine's size, and who serves the directory
|
|
104
|
+
tinystore logs ./data -f # the application's logs, as they arrive
|
|
105
|
+
claude mcp add tinystore -- uvx --from tinyshed-tinystore tinystore mcp ./data # read-only access for an AI agent
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
See [The command line](https://github.com/tinyshed/tinystore/blob/main/docs/running/cli.md)
|
|
109
|
+
for every command.
|
|
110
|
+
|
|
111
|
+
## Documentation
|
|
112
|
+
|
|
113
|
+
- [Getting started](https://github.com/tinyshed/tinystore/blob/main/docs/getting-started.md): install it and write a first program
|
|
114
|
+
- [A tour](https://github.com/tinyshed/tinystore/blob/main/docs/tour.md): every engine on one page
|
|
115
|
+
- [The guides](https://github.com/tinyshed/tinystore/blob/main/docs/README.md): a page per feature, each example in Bun, Python and Go
|
|
116
|
+
- [Python API](https://github.com/tinyshed/tinystore/blob/main/docs/reference/python.md): every call, by engine
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
Apache-2.0.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tinyshed-tinystore"
|
|
3
|
+
version = "0.1.0rc2"
|
|
4
|
+
description = "TinyStore for Python: kv, jobs, blobs, SQL, records and metrics in one directory, served by a sidecar"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = []
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Framework :: AsyncIO",
|
|
12
|
+
"Programming Language :: Python :: 3.12",
|
|
13
|
+
"Programming Language :: Python :: 3.13",
|
|
14
|
+
"Programming Language :: Python :: 3.14",
|
|
15
|
+
"Typing :: Typed",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Repository = "https://github.com/tinyshed/tinystore"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
tinystore = "tinystore._cli:main"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["uv_build>=0.11,<0.12"]
|
|
26
|
+
build-backend = "uv_build"
|
|
27
|
+
|
|
28
|
+
[tool.uv.build-backend]
|
|
29
|
+
module-name = "tinystore"
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
line-length = 120
|
|
33
|
+
target-version = "py312"
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
select = [
|
|
37
|
+
"E",
|
|
38
|
+
"W",
|
|
39
|
+
"F",
|
|
40
|
+
"I",
|
|
41
|
+
"B",
|
|
42
|
+
"UP",
|
|
43
|
+
"SIM",
|
|
44
|
+
"RUF",
|
|
45
|
+
"ASYNC",
|
|
46
|
+
"PT",
|
|
47
|
+
"N",
|
|
48
|
+
"C4",
|
|
49
|
+
"PERF",
|
|
50
|
+
"PIE",
|
|
51
|
+
"RET",
|
|
52
|
+
"TC",
|
|
53
|
+
]
|
|
54
|
+
ignore = [
|
|
55
|
+
"RUF001",
|
|
56
|
+
"RUF002",
|
|
57
|
+
"RUF003",
|
|
58
|
+
"ASYNC109",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint.per-file-ignores]
|
|
62
|
+
"tests/*" = [
|
|
63
|
+
"PT018",
|
|
64
|
+
"PT012",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.format]
|
|
68
|
+
quote-style = "double"
|
|
69
|
+
|
|
70
|
+
[tool.pyright]
|
|
71
|
+
typeCheckingMode = "strict"
|
|
72
|
+
pythonVersion = "3.12"
|
|
73
|
+
include = [
|
|
74
|
+
"src",
|
|
75
|
+
"tests",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
asyncio_mode = "auto"
|
|
80
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
81
|
+
testpaths = ["tests"]
|
|
82
|
+
|
|
83
|
+
[tool.coverage.run]
|
|
84
|
+
branch = true
|
|
85
|
+
source = ["tinystore"]
|
|
86
|
+
|
|
87
|
+
[tool.coverage.report]
|
|
88
|
+
fail_under = 85
|
|
89
|
+
show_missing = true
|
|
90
|
+
skip_covered = true
|
|
91
|
+
|
|
92
|
+
[dependency-groups]
|
|
93
|
+
dev = [
|
|
94
|
+
"pytest>=9.1",
|
|
95
|
+
"pytest-asyncio>=1.4",
|
|
96
|
+
"pytest-cov>=7.1",
|
|
97
|
+
"ruff>=0.16",
|
|
98
|
+
"pyright>=1.1.414",
|
|
99
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tinyshed-tinystore"
|
|
3
|
+
version = "0.1.0rc2"
|
|
4
|
+
description = "TinyStore for Python: kv, jobs, blobs, SQL, records and metrics in one directory, served by a sidecar"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = []
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Framework :: AsyncIO",
|
|
12
|
+
"Programming Language :: Python :: 3.12",
|
|
13
|
+
"Programming Language :: Python :: 3.13",
|
|
14
|
+
"Programming Language :: Python :: 3.14",
|
|
15
|
+
"Typing :: Typed",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Repository = "https://github.com/tinyshed/tinystore"
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
tinystore = "tinystore._cli:main"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["uv_build>=0.11,<0.12"]
|
|
26
|
+
build-backend = "uv_build"
|
|
27
|
+
|
|
28
|
+
[tool.uv.build-backend]
|
|
29
|
+
module-name = "tinystore"
|
|
30
|
+
|
|
31
|
+
[dependency-groups]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=9.1",
|
|
34
|
+
"pytest-asyncio>=1.4",
|
|
35
|
+
"pytest-cov>=7.1",
|
|
36
|
+
"ruff>=0.16",
|
|
37
|
+
"pyright>=1.1.414",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 120
|
|
42
|
+
target-version = "py312"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = [
|
|
46
|
+
"E", "W", "F", "I", "B", "UP", "SIM", "RUF", "ASYNC", "PT", "N", "C4", "PERF", "PIE", "RET", "TC",
|
|
47
|
+
]
|
|
48
|
+
ignore = [
|
|
49
|
+
"RUF001", # an en dash or a multiplication sign in prose is what it says
|
|
50
|
+
"RUF002",
|
|
51
|
+
"RUF003",
|
|
52
|
+
"ASYNC109", # a job's timeout is the handler's, not the call's
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint.per-file-ignores]
|
|
56
|
+
"tests/*" = ["PT018", "PT012"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.format]
|
|
59
|
+
quote-style = "double"
|
|
60
|
+
|
|
61
|
+
[tool.pyright]
|
|
62
|
+
typeCheckingMode = "strict"
|
|
63
|
+
pythonVersion = "3.12"
|
|
64
|
+
include = ["src", "tests"]
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
asyncio_mode = "auto"
|
|
68
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
69
|
+
testpaths = ["tests"]
|
|
70
|
+
|
|
71
|
+
[tool.coverage.run]
|
|
72
|
+
branch = true
|
|
73
|
+
source = ["tinystore"]
|
|
74
|
+
|
|
75
|
+
[tool.coverage.report]
|
|
76
|
+
fail_under = 85
|
|
77
|
+
show_missing = true
|
|
78
|
+
skip_covered = true
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""TinyStore for Python: kv, jobs, blobs, SQL, records and metrics in one directory, served by a sidecar.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
async with tinystore.open("./data") as store:
|
|
6
|
+
sessions = store.kv.bucket("sessions", Session, sliding=timedelta(days=30))
|
|
7
|
+
await sessions.of(user_id).set(token, session)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ._console import ConsoleHandler, handler
|
|
11
|
+
from ._page import Page
|
|
12
|
+
from .blobs import BlobBucket, BlobObject, Download, Usage
|
|
13
|
+
from .clock import Clock
|
|
14
|
+
from .config import Config, Source
|
|
15
|
+
from .errors import (
|
|
16
|
+
CallCancelledError,
|
|
17
|
+
ClosedError,
|
|
18
|
+
ConflictError,
|
|
19
|
+
CorruptError,
|
|
20
|
+
InternalError,
|
|
21
|
+
InUseError,
|
|
22
|
+
InvalidError,
|
|
23
|
+
LimitError,
|
|
24
|
+
OutcomeUnknownError,
|
|
25
|
+
PermissionDeniedError,
|
|
26
|
+
ProtocolError,
|
|
27
|
+
SuspendedError,
|
|
28
|
+
TinystoreError,
|
|
29
|
+
TooNewError,
|
|
30
|
+
TooOldError,
|
|
31
|
+
UnauthenticatedError,
|
|
32
|
+
UnavailableError,
|
|
33
|
+
UnimplementedError,
|
|
34
|
+
)
|
|
35
|
+
from .jobs import ClaimedJob, Enqueue, Job, JobEntry, Queue, Repeat, cron, daily, every
|
|
36
|
+
from .kv import Batch, Bucket, Counters, Entry, Once, Tx
|
|
37
|
+
from .limiter import Allowance, Limiter
|
|
38
|
+
from .metrics import Aggregate, Condition, Plan, Series, none_of, one_of, prefix
|
|
39
|
+
from .quota import Quota, QuotaUsage, WindowUsage
|
|
40
|
+
from .records import Cursor, Record, context, fields, trace
|
|
41
|
+
from .sql import Database, Done
|
|
42
|
+
from .store import Status, Store, connect, open
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"Aggregate",
|
|
46
|
+
"Allowance",
|
|
47
|
+
"Batch",
|
|
48
|
+
"BlobBucket",
|
|
49
|
+
"BlobObject",
|
|
50
|
+
"Bucket",
|
|
51
|
+
"CallCancelledError",
|
|
52
|
+
"ClaimedJob",
|
|
53
|
+
"Clock",
|
|
54
|
+
"ClosedError",
|
|
55
|
+
"Condition",
|
|
56
|
+
"Config",
|
|
57
|
+
"ConflictError",
|
|
58
|
+
"ConsoleHandler",
|
|
59
|
+
"CorruptError",
|
|
60
|
+
"Counters",
|
|
61
|
+
"Cursor",
|
|
62
|
+
"Database",
|
|
63
|
+
"Done",
|
|
64
|
+
"Download",
|
|
65
|
+
"Enqueue",
|
|
66
|
+
"Entry",
|
|
67
|
+
"InUseError",
|
|
68
|
+
"InternalError",
|
|
69
|
+
"InvalidError",
|
|
70
|
+
"Job",
|
|
71
|
+
"JobEntry",
|
|
72
|
+
"LimitError",
|
|
73
|
+
"Limiter",
|
|
74
|
+
"Once",
|
|
75
|
+
"OutcomeUnknownError",
|
|
76
|
+
"Page",
|
|
77
|
+
"PermissionDeniedError",
|
|
78
|
+
"Plan",
|
|
79
|
+
"ProtocolError",
|
|
80
|
+
"Queue",
|
|
81
|
+
"Quota",
|
|
82
|
+
"QuotaUsage",
|
|
83
|
+
"Record",
|
|
84
|
+
"Repeat",
|
|
85
|
+
"Series",
|
|
86
|
+
"Source",
|
|
87
|
+
"Status",
|
|
88
|
+
"Store",
|
|
89
|
+
"SuspendedError",
|
|
90
|
+
"TinystoreError",
|
|
91
|
+
"TooNewError",
|
|
92
|
+
"TooOldError",
|
|
93
|
+
"Tx",
|
|
94
|
+
"UnauthenticatedError",
|
|
95
|
+
"UnavailableError",
|
|
96
|
+
"UnimplementedError",
|
|
97
|
+
"Usage",
|
|
98
|
+
"WindowUsage",
|
|
99
|
+
"connect",
|
|
100
|
+
"context",
|
|
101
|
+
"cron",
|
|
102
|
+
"daily",
|
|
103
|
+
"every",
|
|
104
|
+
"fields",
|
|
105
|
+
"handler",
|
|
106
|
+
"none_of",
|
|
107
|
+
"one_of",
|
|
108
|
+
"open",
|
|
109
|
+
"prefix",
|
|
110
|
+
"trace",
|
|
111
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""What the SDK does in the background, an instrument's flush or a handler's write.
|
|
2
|
+
|
|
3
|
+
It has no caller to tell when it fails, so it says so as Go's tinystore.Store
|
|
4
|
+
does, in TinyStore's own lines: once, again when the failure changes or ten
|
|
5
|
+
minutes on, and once more when the work recovers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import functools
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
14
|
+
|
|
15
|
+
from ._console import ConsoleHandler, Line, encode_fields
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
|
|
20
|
+
_QUIET = 600.0
|
|
21
|
+
"""A failure repeated within this many seconds is counted rather than said again."""
|
|
22
|
+
|
|
23
|
+
type Level = Literal["info", "warn"]
|
|
24
|
+
type Say = Callable[[Level, str, dict[str, Any]], None]
|
|
25
|
+
"""Where TinyStore's own lines go: the console of a handler of stream tinystore, or a test's."""
|
|
26
|
+
|
|
27
|
+
_LEVELS: dict[Level, int] = {"info": 0, "warn": 4}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _Own(ConsoleHandler):
|
|
31
|
+
"""TinyStore's own lines, written as a handler of the console alone writes its own."""
|
|
32
|
+
|
|
33
|
+
def say(self, level: Level, message: str, fields: dict[str, Any]) -> None:
|
|
34
|
+
line = Line(
|
|
35
|
+
at=time.time_ns(),
|
|
36
|
+
stream="tinystore",
|
|
37
|
+
level=_LEVELS[level],
|
|
38
|
+
msg=message,
|
|
39
|
+
attrs=encode_fields(fields.items(), None),
|
|
40
|
+
)
|
|
41
|
+
self._print(line)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@functools.cache
|
|
45
|
+
def _own() -> _Own:
|
|
46
|
+
return _Own("tinystore")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def say_own(level: Level, message: str, fields: dict[str, Any]) -> None:
|
|
50
|
+
"""Says one of TinyStore's own lines, on stderr as the console of a handler of stream tinystore."""
|
|
51
|
+
_own().say(level, message, fields)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FailureLog:
|
|
55
|
+
"""A background work's failures, said as Go's failureLog logs them::
|
|
56
|
+
|
|
57
|
+
00:00 disk full → warn "background work failed" failures=1
|
|
58
|
+
00:01…00:09 the same → counted
|
|
59
|
+
00:10 disk full → warn failures=11
|
|
60
|
+
00:11 success → info "background work recovered" failures=11
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, work: str, say: Say = say_own, now: Callable[[], float] = time.monotonic) -> None:
|
|
64
|
+
self._work, self._say, self._now = work, say, now
|
|
65
|
+
self._last = ""
|
|
66
|
+
self._said_at = 0.0
|
|
67
|
+
self._failures = 0
|
|
68
|
+
|
|
69
|
+
def failed(self, err: BaseException) -> None:
|
|
70
|
+
self._failures += 1
|
|
71
|
+
error = str(err)
|
|
72
|
+
if error == self._last and self._now() - self._said_at < _QUIET:
|
|
73
|
+
return
|
|
74
|
+
self._say("warn", "background work failed", {"work": self._work, "error": error, "failures": self._failures})
|
|
75
|
+
self._last, self._said_at = error, self._now()
|
|
76
|
+
|
|
77
|
+
def succeeded(self) -> None:
|
|
78
|
+
if self._failures > 0:
|
|
79
|
+
self._say("info", "background work recovered", {"work": self._work, "failures": self._failures})
|
|
80
|
+
self._last, self._failures = "", 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DropNotice:
|
|
84
|
+
"""The lines a handler dropped for want of room, said at a flush and at most once in a quiet period.
|
|
85
|
+
|
|
86
|
+
A burst is one line, and a handler that keeps dropping says how many every
|
|
87
|
+
ten minutes rather than every second. A line is dropped on whichever
|
|
88
|
+
thread logged it and said on the loop's, so the count has a lock of its own.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, logger: str, buffer: int, say: Say = say_own, now: Callable[[], float] = time.monotonic) -> None:
|
|
92
|
+
self._logger, self._buffer, self._say, self._now = logger, buffer, say, now
|
|
93
|
+
self._counting = threading.Lock()
|
|
94
|
+
self._unsaid = 0
|
|
95
|
+
self._said_at: float | None = None
|
|
96
|
+
|
|
97
|
+
def dropped(self) -> None:
|
|
98
|
+
with self._counting:
|
|
99
|
+
self._unsaid += 1
|
|
100
|
+
|
|
101
|
+
def say_if_due(self) -> None:
|
|
102
|
+
with self._counting:
|
|
103
|
+
if self._unsaid == 0 or (self._said_at is not None and self._now() - self._said_at < _QUIET):
|
|
104
|
+
return
|
|
105
|
+
dropped, self._unsaid, self._said_at = self._unsaid, 0, self._now()
|
|
106
|
+
# logger, since a console line's own stream is tinystore's
|
|
107
|
+
self._say("warn", "log lines dropped", {"logger": self._logger, "dropped": dropped, "buffer": self._buffer})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""The tinystore command, as this wheel installs it: the binary it carries, run
|
|
2
|
+
with these arguments on this console, its exit code this process's.
|
|
3
|
+
|
|
4
|
+
`uvx --from tinyshed-tinystore tinystore status ./data` needs nothing else
|
|
5
|
+
installed, and after `pip install tinyshed-tinystore` it is `tinystore`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from ._runtime import WINDOWS, packaged_binary
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
# Not PATH's tinystore: that may be this very command, which would run itself.
|
|
19
|
+
binary = os.environ.get("TINYSTORE_BIN") or packaged_binary()
|
|
20
|
+
if binary is None:
|
|
21
|
+
sys.exit(
|
|
22
|
+
"tinystore: this wheel carries no binary for this platform; install a platform's wheel, "
|
|
23
|
+
"put the binary of a release on PATH, or set TINYSTORE_BIN"
|
|
24
|
+
)
|
|
25
|
+
argv = [binary, *sys.argv[1:]]
|
|
26
|
+
if not WINDOWS:
|
|
27
|
+
os.execv(binary, argv) # the binary in this process's place, its signals its own
|
|
28
|
+
sys.exit(_wait(subprocess.Popen(argv)))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _wait(child: subprocess.Popen[bytes]) -> int:
|
|
32
|
+
"""The child's exit code. A Ctrl+C reaches it as it reaches this process, and the child ends on its own,
|
|
33
|
+
letting its SERVE and its lock go, so this one waits rather than end it."""
|
|
34
|
+
while True:
|
|
35
|
+
try:
|
|
36
|
+
return child.wait()
|
|
37
|
+
except KeyboardInterrupt:
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|