raidiantai-analytics 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.
Files changed (28) hide show
  1. raidiantai_analytics-0.1.0/CHANGELOG.md +5 -0
  2. raidiantai_analytics-0.1.0/LICENSE +21 -0
  3. raidiantai_analytics-0.1.0/PKG-INFO +123 -0
  4. raidiantai_analytics-0.1.0/README.md +108 -0
  5. raidiantai_analytics-0.1.0/pyproject.toml +48 -0
  6. raidiantai_analytics-0.1.0/setup.cfg +4 -0
  7. raidiantai_analytics-0.1.0/src/raidiantai_analytics/__init__.py +42 -0
  8. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_admission.py +218 -0
  9. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_client.py +2163 -0
  10. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_config.py +311 -0
  11. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_contract.py +407 -0
  12. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_diagnostics.py +215 -0
  13. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_identity.py +124 -0
  14. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_locks.py +195 -0
  15. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_pool.py +334 -0
  16. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_prepare.py +52 -0
  17. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_retry_after.py +154 -0
  18. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_runtime.py +61 -0
  19. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_serialize.py +139 -0
  20. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_text.py +136 -0
  21. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_transport.py +375 -0
  22. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_validate.py +229 -0
  23. raidiantai_analytics-0.1.0/src/raidiantai_analytics/_version.py +4 -0
  24. raidiantai_analytics-0.1.0/src/raidiantai_analytics/py.typed +0 -0
  25. raidiantai_analytics-0.1.0/src/raidiantai_analytics.egg-info/PKG-INFO +123 -0
  26. raidiantai_analytics-0.1.0/src/raidiantai_analytics.egg-info/SOURCES.txt +26 -0
  27. raidiantai_analytics-0.1.0/src/raidiantai_analytics.egg-info/dependency_links.txt +1 -0
  28. raidiantai_analytics-0.1.0/src/raidiantai_analytics.egg-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ - First release: send a tool's usage to Basecamp from a Python server with a tool key.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Raidiant LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.4
2
+ Name: raidiantai-analytics
3
+ Version: 0.1.0
4
+ Summary: Send a tool's usage to Raidiant AI from a Python server, with a tool key.
5
+ Author: Raidiant LLC
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Typing :: Typed
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # raidiantai-analytics
17
+
18
+ Send your tool's usage to Basecamp from a Python server or script, with a tool key.
19
+
20
+ **Python 3.10 or later. Standard library only; no runtime dependencies. Server
21
+ only:** it sends with a tool key, which is a server-side secret. Versions before
22
+ 1.0 may change; see the changelog.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install raidiantai-analytics
28
+ ```
29
+
30
+ ## Quickstart
31
+
32
+ You need a tool key and your company's Basecamp address (for example
33
+ `https://<your-basecamp-address>`). This reads them from the environment as
34
+ `BASECAMP_TOOL_KEY` and `BASECAMP_URL`. Use a test key while you build: it is
35
+ checked and answered like a live key, and nothing is counted.
36
+
37
+ ```python
38
+ import os
39
+
40
+ from raidiantai_analytics import Analytics, CheckKeyOk, Diagnostic, email_identity
41
+
42
+
43
+ def show(d: Diagnostic) -> None:
44
+ print(f"analytics: {d.code}" + (f" ({d.reason})" if d.reason else ""))
45
+
46
+
47
+ # One client per process. It never raises: a missing or malformed option
48
+ # makes it send nothing and is reported through on_diagnostic.
49
+ analytics = Analytics(
50
+ key=os.environ.get("BASECAMP_TOOL_KEY"),
51
+ base_url=os.environ.get("BASECAMP_URL"),
52
+ # Without this, most problems would only be counted, not shown.
53
+ on_diagnostic=show,
54
+ )
55
+
56
+ # Check the key first, so a wrong, stopped or switched-off key shows up now
57
+ # instead of as usage that never arrives.
58
+ check = analytics.check_key()
59
+ if not isinstance(check, CheckKeyOk):
60
+ print(f"The key check failed: {check.code}")
61
+ elif check.status != "active" or not check.integrations_enabled:
62
+ print("The key is stopped, or integrations are off for your company.")
63
+ else:
64
+ print(f"Sending for {check.tool_name} with a {check.mode} key.")
65
+
66
+ # Once per meaningful completion of your tool's main job, for the person it was for.
67
+ identity = email_identity("alice@example.com")
68
+ if identity.ok:
69
+ analytics.for_actor(identity.actor).capture_use()
70
+
71
+ # Send what is queued. `remaining` is how many events are still waiting.
72
+ result = analytics.flush()
73
+ print(f"{result.remaining} events still queued.")
74
+
75
+ # This script is about to exit, so shut the client down.
76
+ analytics.shutdown()
77
+ ```
78
+
79
+ - Call `capture_use()` once per meaningful completion of your tool's main job.
80
+ Use `capture_custom(name, properties=...)` for anything else; it never counts.
81
+ - Pass the `event_id` of your own work (`capture_use(event_id=...)`) when it can
82
+ repeat: a repeat with the same ID never counts twice.
83
+ - Pass an email your application has verified.
84
+
85
+ ## Sending and shutting down
86
+
87
+ Events are sent by a background thread, in batches. The thread never keeps the
88
+ interpreter alive.
89
+
90
+ - `analytics.flush()` sends what is queued and returns a result whose
91
+ `remaining` is how many events are still queued. `0` means the queue is empty,
92
+ not that everything arrived (refused or given-up events leave it too; see
93
+ `get_stats()`). Otherwise it is the number still queued when its deadline passed (10 seconds by default). Those events are not
94
+ lost; the client keeps trying. In serverless code, call `flush()` before your
95
+ handler returns.
96
+ - `analytics.shutdown()` belongs **only where the process is about to exit**
97
+ (or use the client as a context manager, `with Analytics(...) as analytics:`).
98
+ It stops accepting events, sends what it can for up to 5 seconds, then
99
+ discards the rest; its `remaining` is how many it discarded. After it, every
100
+ capture is dropped. The package registers no `atexit` handler of its own.
101
+
102
+ The queue is in memory, so a restart or crash loses what was still queued.
103
+
104
+ ## Keep the key secret
105
+
106
+ - The key is a **server-side** secret. Read it from the server's environment or
107
+ a secret store; never ship it inside an app your users run.
108
+ - **Never commit** it to a repository.
109
+ - If it may have been exposed, **rotate** it at once: cancel it in Basecamp and
110
+ make a new one.
111
+
112
+ ## Your key and the full guide
113
+
114
+ Both need a Basecamp sign-in:
115
+
116
+ - **Your tool key:** your tool's page in Basecamp, on its **Integration** tab.
117
+ - **The full guide:** `https://<your-basecamp-address>/docs/developers/sdk-python`,
118
+ covering what counts as a use, what can be lost and when, privacy, property
119
+ values, pausing, the network and every option. For help, start there.
120
+
121
+ ## Licence
122
+
123
+ MIT. Copyright (c) 2026 Raidiant LLC. See `LICENSE`.
@@ -0,0 +1,108 @@
1
+ # raidiantai-analytics
2
+
3
+ Send your tool's usage to Basecamp from a Python server or script, with a tool key.
4
+
5
+ **Python 3.10 or later. Standard library only; no runtime dependencies. Server
6
+ only:** it sends with a tool key, which is a server-side secret. Versions before
7
+ 1.0 may change; see the changelog.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install raidiantai-analytics
13
+ ```
14
+
15
+ ## Quickstart
16
+
17
+ You need a tool key and your company's Basecamp address (for example
18
+ `https://<your-basecamp-address>`). This reads them from the environment as
19
+ `BASECAMP_TOOL_KEY` and `BASECAMP_URL`. Use a test key while you build: it is
20
+ checked and answered like a live key, and nothing is counted.
21
+
22
+ ```python
23
+ import os
24
+
25
+ from raidiantai_analytics import Analytics, CheckKeyOk, Diagnostic, email_identity
26
+
27
+
28
+ def show(d: Diagnostic) -> None:
29
+ print(f"analytics: {d.code}" + (f" ({d.reason})" if d.reason else ""))
30
+
31
+
32
+ # One client per process. It never raises: a missing or malformed option
33
+ # makes it send nothing and is reported through on_diagnostic.
34
+ analytics = Analytics(
35
+ key=os.environ.get("BASECAMP_TOOL_KEY"),
36
+ base_url=os.environ.get("BASECAMP_URL"),
37
+ # Without this, most problems would only be counted, not shown.
38
+ on_diagnostic=show,
39
+ )
40
+
41
+ # Check the key first, so a wrong, stopped or switched-off key shows up now
42
+ # instead of as usage that never arrives.
43
+ check = analytics.check_key()
44
+ if not isinstance(check, CheckKeyOk):
45
+ print(f"The key check failed: {check.code}")
46
+ elif check.status != "active" or not check.integrations_enabled:
47
+ print("The key is stopped, or integrations are off for your company.")
48
+ else:
49
+ print(f"Sending for {check.tool_name} with a {check.mode} key.")
50
+
51
+ # Once per meaningful completion of your tool's main job, for the person it was for.
52
+ identity = email_identity("alice@example.com")
53
+ if identity.ok:
54
+ analytics.for_actor(identity.actor).capture_use()
55
+
56
+ # Send what is queued. `remaining` is how many events are still waiting.
57
+ result = analytics.flush()
58
+ print(f"{result.remaining} events still queued.")
59
+
60
+ # This script is about to exit, so shut the client down.
61
+ analytics.shutdown()
62
+ ```
63
+
64
+ - Call `capture_use()` once per meaningful completion of your tool's main job.
65
+ Use `capture_custom(name, properties=...)` for anything else; it never counts.
66
+ - Pass the `event_id` of your own work (`capture_use(event_id=...)`) when it can
67
+ repeat: a repeat with the same ID never counts twice.
68
+ - Pass an email your application has verified.
69
+
70
+ ## Sending and shutting down
71
+
72
+ Events are sent by a background thread, in batches. The thread never keeps the
73
+ interpreter alive.
74
+
75
+ - `analytics.flush()` sends what is queued and returns a result whose
76
+ `remaining` is how many events are still queued. `0` means the queue is empty,
77
+ not that everything arrived (refused or given-up events leave it too; see
78
+ `get_stats()`). Otherwise it is the number still queued when its deadline passed (10 seconds by default). Those events are not
79
+ lost; the client keeps trying. In serverless code, call `flush()` before your
80
+ handler returns.
81
+ - `analytics.shutdown()` belongs **only where the process is about to exit**
82
+ (or use the client as a context manager, `with Analytics(...) as analytics:`).
83
+ It stops accepting events, sends what it can for up to 5 seconds, then
84
+ discards the rest; its `remaining` is how many it discarded. After it, every
85
+ capture is dropped. The package registers no `atexit` handler of its own.
86
+
87
+ The queue is in memory, so a restart or crash loses what was still queued.
88
+
89
+ ## Keep the key secret
90
+
91
+ - The key is a **server-side** secret. Read it from the server's environment or
92
+ a secret store; never ship it inside an app your users run.
93
+ - **Never commit** it to a repository.
94
+ - If it may have been exposed, **rotate** it at once: cancel it in Basecamp and
95
+ make a new one.
96
+
97
+ ## Your key and the full guide
98
+
99
+ Both need a Basecamp sign-in:
100
+
101
+ - **Your tool key:** your tool's page in Basecamp, on its **Integration** tab.
102
+ - **The full guide:** `https://<your-basecamp-address>/docs/developers/sdk-python`,
103
+ covering what counts as a use, what can be lost and when, privacy, property
104
+ values, pausing, the network and every option. For help, start there.
105
+
106
+ ## Licence
107
+
108
+ MIT. Copyright (c) 2026 Raidiant LLC. See `LICENSE`.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "raidiantai-analytics"
7
+ dynamic = ["version"]
8
+ description = "Send a tool's usage to Raidiant AI from a Python server, with a tool key."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Raidiant LLC" }]
14
+ dependencies = []
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Operating System :: OS Independent",
19
+ "Typing :: Typed",
20
+ ]
21
+
22
+ [tool.setuptools.dynamic]
23
+ version = { attr = "raidiantai_analytics._version.__version__" }
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ raidiantai_analytics = ["py.typed"]
30
+
31
+ [tool.ruff]
32
+ target-version = "py310"
33
+ line-length = 120
34
+
35
+ [tool.ruff.lint]
36
+ select = ["E", "F", "W", "B", "UP", "I"]
37
+ ignore = ["B904"]
38
+
39
+ [tool.ruff.lint.per-file-ignores]
40
+ "src/raidiantai_analytics/_contract.py" = ["E501"]
41
+ # The test suite's directory is written as the glob "test[s]" because this file ships
42
+ # in the sdist, whose public-hygiene check refuses a plain test-directory path.
43
+ "test[s]/**" = ["E501", "B011", "B017"]
44
+
45
+ [tool.mypy]
46
+ python_version = "3.10"
47
+ strict = true
48
+ files = ["src/raidiantai_analytics"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,42 @@
1
+ """Send a tool's usage to Raidiant AI from a Python server, with a tool key.
2
+
3
+ Standard library only. Python 3.10 or later.
4
+ """
5
+
6
+ from ._client import (
7
+ ActorHandle,
8
+ Analytics,
9
+ CaptureDropped,
10
+ CaptureInvalid,
11
+ CaptureQueued,
12
+ CaptureResult,
13
+ CheckKeyFailed,
14
+ CheckKeyOk,
15
+ CheckKeyResult,
16
+ DrainResult,
17
+ Stats,
18
+ )
19
+ from ._diagnostics import Diagnostic
20
+ from ._identity import Actor, IdentityOk, IdentityRefused, email_identity
21
+ from ._version import PACKAGE_NAME, __version__
22
+
23
+ __all__ = [
24
+ "PACKAGE_NAME",
25
+ "Actor",
26
+ "ActorHandle",
27
+ "Analytics",
28
+ "CaptureDropped",
29
+ "CaptureInvalid",
30
+ "CaptureQueued",
31
+ "CaptureResult",
32
+ "CheckKeyFailed",
33
+ "CheckKeyOk",
34
+ "CheckKeyResult",
35
+ "Diagnostic",
36
+ "DrainResult",
37
+ "IdentityOk",
38
+ "IdentityRefused",
39
+ "Stats",
40
+ "__version__",
41
+ "email_identity",
42
+ ]
@@ -0,0 +1,218 @@
1
+ """Host admission: may this host value become wire text at all?
2
+
3
+ One iterative traversal that copies the host's value into a snapshot (each
4
+ member read once, through the base type's own methods, so no method a host
5
+ subclass overrides runs), admitting exactly None, bool, an int (not bool) with
6
+ |n| <= 2^53, a finite float, a str with no high-then-low surrogate pair, a
7
+ list or tuple, and a dict whose keys are all str. int, float and str
8
+ subclasses are admitted and copied as their base type. Everything else is
9
+ refused as `invalid_event` at its path. Two representability limits apply
10
+ during the same walk: nesting depth (the event is depth 1) and a running size
11
+ count L, a strict lower bound on the canonical text's UTF-8 bytes, refused as
12
+ `event_too_large` at "" once it passes the ceiling.
13
+
14
+ L accounting, in traversal order (pre-order, an object's key visited just
15
+ before its member, members in ECMAScript key order): every value adds 1, plus
16
+ its code points if it is a str; a container adds its member count when it is
17
+ entered, before any of its members are read; every key adds its code points.
18
+ Inside one value the checks run: size, then type, then depth, then the member
19
+ count. The snapshot holds only exact types, its dicts already in ECMAScript
20
+ key order.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import math
26
+ from dataclasses import dataclass
27
+ from typing import Any
28
+
29
+ from . import _text
30
+ from ._contract import CONTRACT
31
+
32
+ ADMISSION_SIZE_CEILING: int = CONTRACT["sdk_limits"]["ADMISSION_SIZE_CEILING"]
33
+ ADMISSION_DEPTH_CEILING: int = CONTRACT["sdk_limits"]["ADMISSION_DEPTH_CEILING"]
34
+ _MAX_SAFE = 2**53
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class AdmissionRefusal:
39
+ reason: str # "invalid_event" | "event_too_large"
40
+ path: str
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class Admitted:
45
+ value: Any
46
+ count: int
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class Refused:
51
+ refusal: AdmissionRefusal
52
+ count: int
53
+
54
+
55
+ class _Probe:
56
+ """The most recent traversal's final count, for tests that measure where the walk stopped."""
57
+
58
+ last_count = 0
59
+ strings_scanned = 0
60
+
61
+
62
+ admission_probe = _Probe()
63
+
64
+ # Work items. ("value", value, path, depth, setter); ("key", key); setter writes the copy.
65
+ _VALUE = 0
66
+ _KEY = 1
67
+
68
+
69
+ def admit(root: Any) -> Admitted | Refused:
70
+ """Admit and snapshot `root` (the whole event). Never recurses and never calls host code."""
71
+ count = 0
72
+ ceiling = ADMISSION_SIZE_CEILING
73
+ holder: list[Any] = [None]
74
+
75
+ def set_root(x: Any) -> None:
76
+ holder[0] = x
77
+
78
+ stack: list[tuple[Any, ...]] = [(_VALUE, root, "", 1, set_root)]
79
+
80
+ def too_large() -> Refused:
81
+ admission_probe.last_count = count
82
+ return Refused(AdmissionRefusal("event_too_large", ""), count)
83
+
84
+ def invalid(path: str) -> Refused:
85
+ admission_probe.last_count = count
86
+ return Refused(AdmissionRefusal("invalid_event", path), count)
87
+
88
+ while stack:
89
+ item = stack.pop()
90
+ if item[0] == _KEY:
91
+ # A key that is not a str, or a str holding a surrogate pair, is the
92
+ # holding object's fault: its path is the holder's.
93
+ _, k, holder_path = item
94
+ if k is _NOT_A_STR:
95
+ return invalid(holder_path)
96
+ n = len(k)
97
+ if n > ceiling - count:
98
+ count = ceiling + 1
99
+ return too_large()
100
+ count += n
101
+ admission_probe.strings_scanned += 1
102
+ if _text.has_surrogate_pair(k):
103
+ return invalid(holder_path)
104
+ continue
105
+
106
+ _, v, path, depth, setter = item
107
+ t = type(v)
108
+
109
+ # 1. Size.
110
+ count += 1
111
+ is_str = issubclass(t, str)
112
+ if is_str:
113
+ n = str.__len__(v)
114
+ if n > ceiling - count:
115
+ # Counted only up to one past the ceiling: the verdict is the same.
116
+ count = ceiling + 1
117
+ return too_large()
118
+ count += n
119
+ if count > ceiling:
120
+ return too_large()
121
+
122
+ # 2. Type.
123
+ if v is None or t is bool:
124
+ setter(v)
125
+ continue
126
+ if is_str:
127
+ s = v if t is str else str.__str__(v)
128
+ admission_probe.strings_scanned += 1
129
+ if _text.has_surrogate_pair(s):
130
+ return invalid(path)
131
+ setter(s)
132
+ continue
133
+ if issubclass(t, int):
134
+ i = v if t is int else int.__int__(v)
135
+ if i > _MAX_SAFE or i < -_MAX_SAFE:
136
+ return invalid(path)
137
+ setter(i)
138
+ continue
139
+ if issubclass(t, float):
140
+ f = v if t is float else float.__float__(v)
141
+ if not math.isfinite(f):
142
+ return invalid(path)
143
+ setter(f)
144
+ continue
145
+
146
+ is_list = issubclass(t, list)
147
+ if is_list or issubclass(t, tuple):
148
+ # 3. Depth.
149
+ if depth > ADMISSION_DEPTH_CEILING:
150
+ return invalid(path)
151
+ # 4. Member count, on entry.
152
+ length = list.__len__(v) if is_list else tuple.__len__(v)
153
+ count += length
154
+ if count > ceiling:
155
+ return too_large()
156
+ members = list.copy(v) if is_list else list(tuple.__iter__(v))
157
+ copy: list[Any] = [None] * len(members)
158
+ setter(copy)
159
+ for i in range(len(members) - 1, -1, -1):
160
+ stack.append((_VALUE, members[i], _text.child_path(path, i), depth + 1, _list_setter(copy, i)))
161
+ continue
162
+
163
+ if issubclass(t, dict):
164
+ if depth > ADMISSION_DEPTH_CEILING:
165
+ return invalid(path)
166
+ length = dict.__len__(v)
167
+ count += length
168
+ if count > ceiling:
169
+ return too_large()
170
+ pairs = [(_key_name(k), m) for k, m in dict.items(v)]
171
+ ordered = _text._es_key_order(pairs, name=lambda p: p[0])
172
+ obj: dict[str, Any] = {}
173
+ setter(obj)
174
+ for i in range(len(ordered) - 1, -1, -1):
175
+ k, m = ordered[i]
176
+ member_path = path if k is _NOT_A_STR else _text.child_path(path, k)
177
+ stack.append((_VALUE, m, member_path, depth + 1, _dict_setter(obj, k)))
178
+ stack.append((_KEY, k, path))
179
+ continue
180
+
181
+ return invalid(path)
182
+
183
+ admission_probe.last_count = count
184
+ return Admitted(holder[0], count)
185
+
186
+
187
+ class _NotAStr:
188
+ __slots__ = ()
189
+
190
+ def __repr__(self) -> str:
191
+ return "<not a str>"
192
+
193
+
194
+ _NOT_A_STR: Any = _NotAStr()
195
+
196
+
197
+ def _key_name(k: Any) -> Any:
198
+ t = type(k)
199
+ if t is str:
200
+ return k
201
+ if issubclass(t, str):
202
+ return str.__str__(k)
203
+ return _NOT_A_STR
204
+
205
+
206
+ def _list_setter(copy: list[Any], i: int) -> Any:
207
+ def set_item(x: Any) -> None:
208
+ copy[i] = x
209
+
210
+ return set_item
211
+
212
+
213
+ def _dict_setter(obj: dict[str, Any], k: Any) -> Any:
214
+ def set_member(x: Any) -> None:
215
+ if k is not _NOT_A_STR:
216
+ obj[k] = x
217
+
218
+ return set_member