fluidlayer 2.1.0__tar.gz → 2.2.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 (89) hide show
  1. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/PKG-INFO +1 -1
  2. fluidlayer-2.2.0/deployment/__init__.py +1 -0
  3. fluidlayer-2.2.0/deployment/session_registry.py +178 -0
  4. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/CHANGELOG.md +11 -0
  5. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/__init__.py +1 -1
  6. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer.egg-info/PKG-INFO +1 -1
  7. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer.egg-info/SOURCES.txt +3 -0
  8. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer.egg-info/top_level.txt +1 -0
  9. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/pyproject.toml +2 -2
  10. fluidlayer-2.2.0/tests/test_session_registry.py +126 -0
  11. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/CITATION.cff +0 -0
  12. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/LICENSE +0 -0
  13. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/MANIFEST.in +0 -0
  14. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/Makefile +0 -0
  15. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/README.md +0 -0
  16. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/API.md +0 -0
  17. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/BLOG.md +0 -0
  18. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/COMPARISON.md +0 -0
  19. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/DESIGN_pinned_memory.md +0 -0
  20. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/FRAMEWORK.md +0 -0
  21. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/GETTING_STARTED.md +0 -0
  22. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/GRAFT.md +0 -0
  23. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/LIMITATION.md +0 -0
  24. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/MILESTONES.md +0 -0
  25. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/NEGATIVE-RESULTS.md +0 -0
  26. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/POSITIONING.md +0 -0
  27. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/PROCESS.md +0 -0
  28. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/README.md +0 -0
  29. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/REPRODUCING.md +0 -0
  30. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/TEN-MILLION.md +0 -0
  31. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/docs/index.html +0 -0
  32. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/examples/appkit/attach_layer.py +0 -0
  33. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/examples/appkit/load_model.py +0 -0
  34. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/examples/gate_demo.py +0 -0
  35. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/examples/graft_demo.py +0 -0
  36. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/examples/quickstart.py +0 -0
  37. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e10_gate.py +0 -0
  38. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e11_exact.py +0 -0
  39. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e12_router.py +0 -0
  40. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e13_knn_baseline.py +0 -0
  41. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e14_real_gpt2.py +0 -0
  42. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e15_scale_model.py +0 -0
  43. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e1_prior_perplexity.py +0 -0
  44. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e2_scaling.py +0 -0
  45. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e4_gbalance.py +0 -0
  46. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e5_graft.py +0 -0
  47. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e6_graft.py +0 -0
  48. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e6_real_corpus.py +0 -0
  49. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e7_lossy_fix.py +0 -0
  50. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/e9_ten_million.py +0 -0
  51. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/gpu_graft.py +0 -0
  52. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/pretrain_prior.py +0 -0
  53. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/experiments/run_all.py +0 -0
  54. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/autodiff.py +0 -0
  55. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/blocks.py +0 -0
  56. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/bpe.py +0 -0
  57. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/data.py +0 -0
  58. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/diffusion.py +0 -0
  59. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/gpt2_model.py +0 -0
  60. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/gpt2_tokenizer.py +0 -0
  61. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/graft.py +0 -0
  62. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/losses.py +0 -0
  63. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/memory.py +0 -0
  64. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/model.py +0 -0
  65. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/optim.py +0 -0
  66. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/py.typed +0 -0
  67. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/qwen2_model.py +0 -0
  68. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/sheaf.py +0 -0
  69. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/topology.py +0 -0
  70. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer/torch_graft.py +0 -0
  71. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer.egg-info/dependency_links.txt +0 -0
  72. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer.egg-info/requires.txt +0 -0
  73. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/fluidlayer_single.py +0 -0
  74. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/requirements.txt +0 -0
  75. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/run_without_pytest.py +0 -0
  76. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/selftest.py +0 -0
  77. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/setup.cfg +0 -0
  78. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/conftest.py +0 -0
  79. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_bpe.py +0 -0
  80. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_graft.py +0 -0
  81. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_memory.py +0 -0
  82. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_qwen2_model.py +0 -0
  83. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_selftest.py +0 -0
  84. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_topology.py +0 -0
  85. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tests/test_torch_parity.py +0 -0
  86. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tools/_bundle_template.py +0 -0
  87. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tools/bundle_single_file.py +0 -0
  88. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tools/download_gpt2.py +0 -0
  89. {fluidlayer-2.1.0 → fluidlayer-2.2.0}/tools/fetch_pypi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluidlayer
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: A research memory layer for language models with optional GPT-2 and Qwen2 adapters
5
5
  Author: Kapil Dhinwa
6
6
  License: MIT
@@ -0,0 +1 @@
1
+ """Optional deployment helpers for applications embedding FluidLayer."""
@@ -0,0 +1,178 @@
1
+ """Bounded, process-local isolation for stateful per-session memory branches.
2
+
3
+ This helper is an application integration aid, not an authentication system
4
+ or a cross-process session store. Applications must derive identifiers from
5
+ their authenticated identity layer and must not expose branch objects beyond
6
+ the lease that owns them.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+ import threading
13
+ import time
14
+ from collections.abc import Iterator
15
+ from contextlib import contextmanager
16
+ from dataclasses import dataclass, field
17
+ from typing import Callable, Generic, Protocol, TypeVar
18
+
19
+
20
+ class Resettable(Protocol):
21
+ """The minimum interface required from a stateful branch."""
22
+
23
+ def reset(self) -> None:
24
+ """Clear all state associated with the branch."""
25
+
26
+
27
+ BranchT = TypeVar("BranchT", bound=Resettable)
28
+
29
+
30
+ class SessionRegistryError(RuntimeError):
31
+ """Base exception for session-registry lifecycle errors."""
32
+
33
+
34
+ class SessionCapacityError(SessionRegistryError):
35
+ """Raised when the registry is full of unexpired sessions."""
36
+
37
+
38
+ class SessionBusyError(SessionRegistryError):
39
+ """Raised when a caller tries to reset or close an active session."""
40
+
41
+
42
+ @dataclass
43
+ class _Session(Generic[BranchT]):
44
+ branch: BranchT
45
+ last_used: float
46
+ active_leases: int = 0
47
+ lock: threading.RLock = field(default_factory=threading.RLock)
48
+
49
+
50
+ class SessionRegistry(Generic[BranchT]):
51
+ """Manage separate stateful branches for `(tenant_id, session_id)` keys.
52
+
53
+ Each lease holds a per-session lock for its whole duration. This prevents
54
+ concurrent requests from interleaving writes to one branch while allowing
55
+ independent sessions to proceed concurrently. Expired idle sessions are
56
+ removed when the registry is next accessed. Capacity is a hard bound:
57
+ active or unexpired sessions are never silently evicted.
58
+
59
+ The registry is process-local and volatile. For multiple workers, use
60
+ worker affinity or an application-owned session service; do not serialize
61
+ live branch objects and assume they are safe to restore.
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ branch_factory: Callable[[], BranchT],
67
+ *,
68
+ max_sessions: int = 1024,
69
+ ttl_seconds: float = 1800.0,
70
+ clock: Callable[[], float] = time.monotonic,
71
+ ) -> None:
72
+ if not callable(branch_factory):
73
+ raise TypeError("branch_factory must be callable")
74
+ if isinstance(max_sessions, bool) or not isinstance(max_sessions, int):
75
+ raise TypeError("max_sessions must be an integer")
76
+ if max_sessions <= 0:
77
+ raise ValueError("max_sessions must be greater than zero")
78
+ if isinstance(ttl_seconds, bool) or not isinstance(ttl_seconds, (int, float)):
79
+ raise TypeError("ttl_seconds must be a finite positive number")
80
+ if not math.isfinite(ttl_seconds) or ttl_seconds <= 0:
81
+ raise ValueError("ttl_seconds must be a finite positive number")
82
+ if not callable(clock):
83
+ raise TypeError("clock must be callable")
84
+
85
+ self._branch_factory = branch_factory
86
+ self._max_sessions = max_sessions
87
+ self._ttl_seconds = float(ttl_seconds)
88
+ self._clock = clock
89
+ self._sessions: dict[tuple[str, str], _Session[BranchT]] = {}
90
+ self._lock = threading.RLock()
91
+
92
+ @staticmethod
93
+ def _validate_id(value: str, name: str) -> str:
94
+ if not isinstance(value, str):
95
+ raise TypeError(f"{name} must be a string")
96
+ if not value.strip():
97
+ raise ValueError(f"{name} must not be empty")
98
+ return value
99
+
100
+ def _expire_idle_sessions(self, now: float) -> None:
101
+ expired = [
102
+ key
103
+ for key, session in self._sessions.items()
104
+ if session.active_leases == 0
105
+ and now - session.last_used >= self._ttl_seconds
106
+ ]
107
+ for key in expired:
108
+ del self._sessions[key]
109
+
110
+ @contextmanager
111
+ def lease(self, tenant_id: str, session_id: str) -> Iterator[BranchT]:
112
+ """Yield the branch for one identity pair, serialized per session."""
113
+ tenant = self._validate_id(tenant_id, "tenant_id")
114
+ session_name = self._validate_id(session_id, "session_id")
115
+ key = (tenant, session_name)
116
+ now = self._clock()
117
+
118
+ with self._lock:
119
+ self._expire_idle_sessions(now)
120
+ session = self._sessions.get(key)
121
+ if session is None:
122
+ if len(self._sessions) >= self._max_sessions:
123
+ raise SessionCapacityError(
124
+ f"session registry is at capacity "
125
+ f"({self._max_sessions}); retry after sessions expire "
126
+ "or close an idle session"
127
+ )
128
+ session = _Session(self._branch_factory(), last_used=now)
129
+ self._sessions[key] = session
130
+ session.active_leases += 1
131
+ session.last_used = now
132
+
133
+ try:
134
+ with session.lock:
135
+ yield session.branch
136
+ finally:
137
+ with self._lock:
138
+ session.active_leases -= 1
139
+ session.last_used = self._clock()
140
+
141
+ def reset_session(self, tenant_id: str, session_id: str) -> bool:
142
+ """Reset an idle session; return False if it does not exist."""
143
+ key = (
144
+ self._validate_id(tenant_id, "tenant_id"),
145
+ self._validate_id(session_id, "session_id"),
146
+ )
147
+ with self._lock:
148
+ self._expire_idle_sessions(self._clock())
149
+ session = self._sessions.get(key)
150
+ if session is None:
151
+ return False
152
+ if session.active_leases:
153
+ raise SessionBusyError("cannot reset a session with an active lease")
154
+ session.branch.reset()
155
+ session.last_used = self._clock()
156
+ return True
157
+
158
+ def close_session(self, tenant_id: str, session_id: str) -> bool:
159
+ """Remove an idle session; return False if it does not exist."""
160
+ key = (
161
+ self._validate_id(tenant_id, "tenant_id"),
162
+ self._validate_id(session_id, "session_id"),
163
+ )
164
+ with self._lock:
165
+ session = self._sessions.get(key)
166
+ if session is None:
167
+ return False
168
+ if session.active_leases:
169
+ raise SessionBusyError("cannot close a session with an active lease")
170
+ del self._sessions[key]
171
+ return True
172
+
173
+ @property
174
+ def session_count(self) -> int:
175
+ """Return the number of registered sessions after expiring idle entries."""
176
+ with self._lock:
177
+ self._expire_idle_sessions(self._clock())
178
+ return len(self._sessions)
@@ -6,6 +6,17 @@ uses [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.2.0] - 2026-11-03
10
+
11
+ ### Added
12
+
13
+ - Include the optional deployment session-isolation companion in the
14
+ published wheel and source distribution.
15
+
16
+ ### Changed
17
+
18
+ - Add clean-install CI verification for the deployment package.
19
+
9
20
  ## [2.1.0] - 2026-11-03
10
21
 
11
22
  ### Added
@@ -70,7 +70,7 @@ from .topology import (
70
70
  unique_edges,
71
71
  )
72
72
 
73
- __version__ = "2.1.0"
73
+ __version__ = "2.2.0"
74
74
  version = __version__
75
75
 
76
76
  __all__ = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fluidlayer
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: A research memory layer for language models with optional GPT-2 and Qwen2 adapters
5
5
  Author: Kapil Dhinwa
6
6
  License: MIT
@@ -8,6 +8,8 @@ pyproject.toml
8
8
  requirements.txt
9
9
  run_without_pytest.py
10
10
  selftest.py
11
+ deployment/__init__.py
12
+ deployment/session_registry.py
11
13
  docs/API.md
12
14
  docs/BLOG.md
13
15
  docs/CHANGELOG.md
@@ -76,6 +78,7 @@ tests/test_graft.py
76
78
  tests/test_memory.py
77
79
  tests/test_qwen2_model.py
78
80
  tests/test_selftest.py
81
+ tests/test_session_registry.py
79
82
  tests/test_topology.py
80
83
  tests/test_torch_parity.py
81
84
  tools/_bundle_template.py
@@ -1 +1,2 @@
1
+ deployment
1
2
  fluidlayer
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fluidlayer"
7
- version = "2.1.0"
7
+ version = "2.2.0"
8
8
  description = "A research memory layer for language models with optional GPT-2 and Qwen2 adapters"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -50,7 +50,7 @@ Issues = "https://github.com/kapildhinwa2010-dev/fluidtech/issues"
50
50
  Changelog = "https://github.com/kapildhinwa2010-dev/fluidtech/blob/main/docs/CHANGELOG.md"
51
51
 
52
52
  [tool.setuptools]
53
- packages = ["fluidlayer"]
53
+ packages = ["fluidlayer", "deployment"]
54
54
  include-package-data = true
55
55
 
56
56
  [tool.setuptools.package-data]
@@ -0,0 +1,126 @@
1
+ import threading
2
+
3
+ import pytest
4
+
5
+ from deployment.session_registry import (
6
+ SessionBusyError,
7
+ SessionCapacityError,
8
+ SessionRegistry,
9
+ )
10
+
11
+
12
+ class DummyBranch:
13
+ def __init__(self):
14
+ self.values = []
15
+ self.reset_count = 0
16
+
17
+ def reset(self):
18
+ self.values.clear()
19
+ self.reset_count += 1
20
+
21
+
22
+ def test_sessions_are_isolated_by_tenant_and_session():
23
+ registry = SessionRegistry(DummyBranch)
24
+
25
+ with registry.lease("tenant-a", "conversation") as first:
26
+ first.values.append("private")
27
+ with registry.lease("tenant-b", "conversation") as second:
28
+ assert second.values == []
29
+ second.values.append("other")
30
+ with registry.lease("tenant-a", "conversation") as first_again:
31
+ assert first_again is first
32
+ assert first_again.values == ["private"]
33
+
34
+
35
+ def test_capacity_is_a_hard_bound_and_expired_sessions_are_reclaimed():
36
+ now = [0.0]
37
+ registry = SessionRegistry(
38
+ DummyBranch, max_sessions=1, ttl_seconds=5, clock=lambda: now[0]
39
+ )
40
+ with registry.lease("tenant", "one"):
41
+ pass
42
+
43
+ with pytest.raises(SessionCapacityError):
44
+ with registry.lease("tenant", "two"):
45
+ pass
46
+
47
+ now[0] = 5.0
48
+ with registry.lease("tenant", "two"):
49
+ assert registry.session_count == 1
50
+
51
+
52
+ def test_active_leases_are_not_expired():
53
+ now = [0.0]
54
+ registry = SessionRegistry(DummyBranch, ttl_seconds=2, clock=lambda: now[0])
55
+
56
+ with registry.lease("tenant", "one"):
57
+ now[0] = 100.0
58
+ assert registry.session_count == 1
59
+
60
+
61
+ def test_reset_and_close_require_an_idle_session():
62
+ registry = SessionRegistry(DummyBranch)
63
+ with registry.lease("tenant", "one") as branch:
64
+ branch.values.append("value")
65
+ with pytest.raises(SessionBusyError):
66
+ registry.reset_session("tenant", "one")
67
+ with pytest.raises(SessionBusyError):
68
+ registry.close_session("tenant", "one")
69
+
70
+ assert registry.reset_session("tenant", "one")
71
+ with registry.lease("tenant", "one") as reset_branch:
72
+ assert reset_branch.values == []
73
+ assert reset_branch.reset_count == 1
74
+ assert registry.close_session("tenant", "one")
75
+ assert not registry.close_session("tenant", "one")
76
+
77
+
78
+ def test_requests_for_one_session_are_serialized():
79
+ registry = SessionRegistry(DummyBranch)
80
+ first_entered = threading.Event()
81
+ release_first = threading.Event()
82
+ second_entered = threading.Event()
83
+
84
+ def hold_first_lease():
85
+ with registry.lease("tenant", "same"):
86
+ first_entered.set()
87
+ assert release_first.wait(timeout=2)
88
+
89
+ def acquire_second_lease():
90
+ with registry.lease("tenant", "same"):
91
+ second_entered.set()
92
+
93
+ first = threading.Thread(target=hold_first_lease)
94
+ second = threading.Thread(target=acquire_second_lease)
95
+ first.start()
96
+ assert first_entered.wait(timeout=2)
97
+ second.start()
98
+ assert not second_entered.wait(timeout=0.05)
99
+ release_first.set()
100
+ first.join(timeout=2)
101
+ second.join(timeout=2)
102
+
103
+ assert not first.is_alive()
104
+ assert not second.is_alive()
105
+ assert second_entered.is_set()
106
+
107
+
108
+ @pytest.mark.parametrize(
109
+ ("kwargs", "error"),
110
+ [
111
+ ({"max_sessions": 0}, ValueError),
112
+ ({"max_sessions": True}, TypeError),
113
+ ({"ttl_seconds": 0}, ValueError),
114
+ ({"ttl_seconds": float("inf")}, ValueError),
115
+ ],
116
+ )
117
+ def test_configuration_is_validated(kwargs, error):
118
+ with pytest.raises(error):
119
+ SessionRegistry(DummyBranch, **kwargs)
120
+
121
+
122
+ def test_blank_session_identifiers_are_rejected():
123
+ registry = SessionRegistry(DummyBranch)
124
+ with pytest.raises(ValueError):
125
+ with registry.lease("tenant", " "):
126
+ pass
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes