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.
Files changed (33) hide show
  1. tinyshed_tinystore-0.1.0rc2/PKG-INFO +135 -0
  2. tinyshed_tinystore-0.1.0rc2/README.md +120 -0
  3. tinyshed_tinystore-0.1.0rc2/pyproject.toml +99 -0
  4. tinyshed_tinystore-0.1.0rc2/pyproject.toml.orig +78 -0
  5. tinyshed_tinystore-0.1.0rc2/src/tinystore/__init__.py +111 -0
  6. tinyshed_tinystore-0.1.0rc2/src/tinystore/_background.py +107 -0
  7. tinyshed_tinystore-0.1.0rc2/src/tinystore/_cli.py +42 -0
  8. tinyshed_tinystore-0.1.0rc2/src/tinystore/_connection.py +446 -0
  9. tinyshed_tinystore-0.1.0rc2/src/tinystore/_console.py +310 -0
  10. tinyshed_tinystore-0.1.0rc2/src/tinystore/_page.py +16 -0
  11. tinyshed_tinystore-0.1.0rc2/src/tinystore/_runtime.py +177 -0
  12. tinyshed_tinystore-0.1.0rc2/src/tinystore/_session.py +503 -0
  13. tinyshed_tinystore-0.1.0rc2/src/tinystore/_time.py +48 -0
  14. tinyshed_tinystore-0.1.0rc2/src/tinystore/_trace.py +11 -0
  15. tinyshed_tinystore-0.1.0rc2/src/tinystore/_values.py +113 -0
  16. tinyshed_tinystore-0.1.0rc2/src/tinystore/_wire/__init__.py +1 -0
  17. tinyshed_tinystore-0.1.0rc2/src/tinystore/_wire/codec.py +305 -0
  18. tinyshed_tinystore-0.1.0rc2/src/tinystore/_wire/frame.py +107 -0
  19. tinyshed_tinystore-0.1.0rc2/src/tinystore/_wire/messages.py +535 -0
  20. tinyshed_tinystore-0.1.0rc2/src/tinystore/_wire/msgpack.py +483 -0
  21. tinyshed_tinystore-0.1.0rc2/src/tinystore/blobs.py +345 -0
  22. tinyshed_tinystore-0.1.0rc2/src/tinystore/clock.py +47 -0
  23. tinyshed_tinystore-0.1.0rc2/src/tinystore/config.py +467 -0
  24. tinyshed_tinystore-0.1.0rc2/src/tinystore/errors.py +175 -0
  25. tinyshed_tinystore-0.1.0rc2/src/tinystore/jobs.py +742 -0
  26. tinyshed_tinystore-0.1.0rc2/src/tinystore/kv.py +791 -0
  27. tinyshed_tinystore-0.1.0rc2/src/tinystore/limiter.py +73 -0
  28. tinyshed_tinystore-0.1.0rc2/src/tinystore/metrics.py +719 -0
  29. tinyshed_tinystore-0.1.0rc2/src/tinystore/py.typed +0 -0
  30. tinyshed_tinystore-0.1.0rc2/src/tinystore/quota.py +104 -0
  31. tinyshed_tinystore-0.1.0rc2/src/tinystore/records.py +574 -0
  32. tinyshed_tinystore-0.1.0rc2/src/tinystore/sql.py +321 -0
  33. 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()