globalstash 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
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,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: globalstash
3
+ Version: 0.1.0
4
+ Summary: Thread-safe, process-wide global state you can import from any module at any depth.
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/your-username/globalstash
8
+ Keywords: global,state,flask.g,config,singleton,thread-safe
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # globalstash
20
+
21
+ Thread-safe, process-wide global state you can import from **any module at any depth**, with no `sys.path` tricks.
22
+
23
+ ```bash
24
+ pip install globalstash
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ from globalstash import g
31
+
32
+ # anywhere in your app
33
+ g.db_pool = create_pool()
34
+ g["config"] = {"debug": True}
35
+
36
+ g.get("missing", "default")
37
+ g.setdefault("cache", {})
38
+ g.update(env="prod", version="1.0")
39
+ "db_pool" in g
40
+ del g.db_pool
41
+ ```
42
+
43
+ Because Python caches modules, every `from globalstash import g` returns the same object.
44
+
45
+ ### Atomic and compound operations
46
+
47
+ ```python
48
+ g.incr("requests") # atomic counter
49
+
50
+ with g.lock: # read-modify-write
51
+ g.total = g.total + 1
52
+ ```
53
+
54
+ ### Named namespaces
55
+
56
+ ```python
57
+ from globalstash import get_state
58
+
59
+ cache = get_state("cache") # same name -> same object everywhere
60
+ cache.ttl = 60
61
+ ```
62
+
63
+ ### Tests
64
+
65
+ ```python
66
+ with g.temporary(debug=True):
67
+ ... # values restored on exit
68
+ ```
69
+
70
+ ## Notes
71
+
72
+ - Method names (`get`, `pop`, `update`, ...) can't be used as attribute keys; use `g["get"]`.
73
+ - State lives **per process**. Gunicorn/Uvicorn workers each have their own copy. Use Redis or a database for state shared across processes.
74
+ - For per-request state (like `flask.g`), use `contextvars` instead.
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,60 @@
1
+ # globalstash
2
+
3
+ Thread-safe, process-wide global state you can import from **any module at any depth**, with no `sys.path` tricks.
4
+
5
+ ```bash
6
+ pip install globalstash
7
+ ```
8
+
9
+ ## Usage
10
+
11
+ ```python
12
+ from globalstash import g
13
+
14
+ # anywhere in your app
15
+ g.db_pool = create_pool()
16
+ g["config"] = {"debug": True}
17
+
18
+ g.get("missing", "default")
19
+ g.setdefault("cache", {})
20
+ g.update(env="prod", version="1.0")
21
+ "db_pool" in g
22
+ del g.db_pool
23
+ ```
24
+
25
+ Because Python caches modules, every `from globalstash import g` returns the same object.
26
+
27
+ ### Atomic and compound operations
28
+
29
+ ```python
30
+ g.incr("requests") # atomic counter
31
+
32
+ with g.lock: # read-modify-write
33
+ g.total = g.total + 1
34
+ ```
35
+
36
+ ### Named namespaces
37
+
38
+ ```python
39
+ from globalstash import get_state
40
+
41
+ cache = get_state("cache") # same name -> same object everywhere
42
+ cache.ttl = 60
43
+ ```
44
+
45
+ ### Tests
46
+
47
+ ```python
48
+ with g.temporary(debug=True):
49
+ ... # values restored on exit
50
+ ```
51
+
52
+ ## Notes
53
+
54
+ - Method names (`get`, `pop`, `update`, ...) can't be used as attribute keys; use `g["get"]`.
55
+ - State lives **per process**. Gunicorn/Uvicorn workers each have their own copy. Use Redis or a database for state shared across processes.
56
+ - For per-request state (like `flask.g`), use `contextvars` instead.
57
+
58
+ ## License
59
+
60
+ MIT
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "globalstash"
7
+ version = "0.1.0"
8
+ description = "Thread-safe, process-wide global state you can import from any module at any depth."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Your Name", email = "you@example.com" }]
13
+ keywords = ["global", "state", "flask.g", "config", "singleton", "thread-safe"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Typing :: Typed",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/your-username/globalstash"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ globalstash = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """globalstash: thread-safe process-wide global state."""
2
+ from .core import GlobalState, g, get_state
3
+
4
+ __all__ = ["GlobalState", "g", "get_state"]
5
+ __version__ = "0.1.0"
@@ -0,0 +1,178 @@
1
+ """Core implementation of globalstash."""
2
+ from __future__ import annotations
3
+
4
+ import threading
5
+ from contextlib import contextmanager
6
+ from typing import Any, Dict, Iterator, Tuple
7
+
8
+ _MISSING = object()
9
+
10
+
11
+ class GlobalState:
12
+ """A thread-safe namespace for process-wide global values.
13
+
14
+ Values can be accessed as attributes or as items::
15
+
16
+ g.db = create_pool()
17
+ g["db"]
18
+ g.get("missing", None)
19
+
20
+ Individual operations are atomic. For compound operations
21
+ (read-modify-write), use ``incr`` or the ``lock`` context manager::
22
+
23
+ with g.lock:
24
+ g.total = g.total + 1
25
+
26
+ Note: names of methods (``get``, ``pop``, ``update``...) can't be used
27
+ as attribute keys; use item access (``g["get"]``) for those.
28
+ """
29
+
30
+ __slots__ = ("_data", "_lock")
31
+
32
+ def __init__(self, **initial: Any) -> None:
33
+ object.__setattr__(self, "_data", dict(initial))
34
+ object.__setattr__(self, "_lock", threading.RLock())
35
+
36
+ # ---- attribute access -------------------------------------------------
37
+ def __getattr__(self, name: str) -> Any:
38
+ if name.startswith("__") or name in ("_data", "_lock"):
39
+ raise AttributeError(name)
40
+ with self._lock:
41
+ try:
42
+ return self._data[name]
43
+ except KeyError:
44
+ raise AttributeError(
45
+ f"global state has no attribute {name!r}"
46
+ ) from None
47
+
48
+ def __setattr__(self, name: str, value: Any) -> None:
49
+ with self._lock:
50
+ self._data[name] = value
51
+
52
+ def __delattr__(self, name: str) -> None:
53
+ with self._lock:
54
+ try:
55
+ del self._data[name]
56
+ except KeyError:
57
+ raise AttributeError(name) from None
58
+
59
+ # ---- item access ------------------------------------------------------
60
+ def __getitem__(self, key: str) -> Any:
61
+ with self._lock:
62
+ return self._data[key]
63
+
64
+ def __setitem__(self, key: str, value: Any) -> None:
65
+ with self._lock:
66
+ self._data[key] = value
67
+
68
+ def __delitem__(self, key: str) -> None:
69
+ with self._lock:
70
+ del self._data[key]
71
+
72
+ def __contains__(self, key: object) -> bool:
73
+ with self._lock:
74
+ return key in self._data
75
+
76
+ def __iter__(self) -> Iterator[str]:
77
+ with self._lock:
78
+ return iter(list(self._data))
79
+
80
+ def __len__(self) -> int:
81
+ with self._lock:
82
+ return len(self._data)
83
+
84
+ def __repr__(self) -> str:
85
+ with self._lock:
86
+ return f"GlobalState({self._data!r})"
87
+
88
+ # ---- dict-like helpers ------------------------------------------------
89
+ def get(self, key: str, default: Any = None) -> Any:
90
+ with self._lock:
91
+ return self._data.get(key, default)
92
+
93
+ def set(self, key: str, value: Any) -> None:
94
+ self[key] = value
95
+
96
+ def setdefault(self, key: str, default: Any = None) -> Any:
97
+ with self._lock:
98
+ return self._data.setdefault(key, default)
99
+
100
+ def pop(self, key: str, default: Any = _MISSING) -> Any:
101
+ with self._lock:
102
+ if default is _MISSING:
103
+ return self._data.pop(key)
104
+ return self._data.pop(key, default)
105
+
106
+ def update(self, *args: Any, **kwargs: Any) -> None:
107
+ with self._lock:
108
+ self._data.update(*args, **kwargs)
109
+
110
+ def keys(self) -> Tuple[str, ...]:
111
+ with self._lock:
112
+ return tuple(self._data.keys())
113
+
114
+ def values(self) -> Tuple[Any, ...]:
115
+ with self._lock:
116
+ return tuple(self._data.values())
117
+
118
+ def items(self) -> Tuple[Tuple[str, Any], ...]:
119
+ with self._lock:
120
+ return tuple(self._data.items())
121
+
122
+ def clear(self) -> None:
123
+ with self._lock:
124
+ self._data.clear()
125
+
126
+ def to_dict(self) -> Dict[str, Any]:
127
+ """Return a shallow copy of the current state."""
128
+ with self._lock:
129
+ return dict(self._data)
130
+
131
+ # ---- atomic / compound operations ------------------------------------
132
+ def incr(self, key: str, amount: int = 1, default: int = 0) -> int:
133
+ """Atomically add ``amount`` to ``key`` and return the new value."""
134
+ with self._lock:
135
+ value = self._data.get(key, default) + amount
136
+ self._data[key] = value
137
+ return value
138
+
139
+ @property
140
+ def lock(self) -> "threading.RLock": # type: ignore[valid-type]
141
+ """The re-entrant lock, for compound operations: ``with g.lock: ...``"""
142
+ return self._lock
143
+
144
+ @contextmanager
145
+ def temporary(self, **overrides: Any) -> Iterator["GlobalState"]:
146
+ """Temporarily override values (handy in tests); restored on exit."""
147
+ with self._lock:
148
+ saved = {k: self._data.get(k, _MISSING) for k in overrides}
149
+ self._data.update(overrides)
150
+ try:
151
+ yield self
152
+ finally:
153
+ with self._lock:
154
+ for k, v in saved.items():
155
+ if v is _MISSING:
156
+ self._data.pop(k, None)
157
+ else:
158
+ self._data[k] = v
159
+
160
+
161
+ # ---- default instance and named registry ---------------------------------
162
+ g = GlobalState()
163
+
164
+ _registry: Dict[str, GlobalState] = {}
165
+ _registry_lock = threading.Lock()
166
+
167
+
168
+ def get_state(name: str) -> GlobalState:
169
+ """Return the shared ``GlobalState`` registered under ``name``.
170
+
171
+ The same name always returns the same object, so independent libraries
172
+ or components can each keep their own namespace without collisions.
173
+ """
174
+ with _registry_lock:
175
+ state = _registry.get(name)
176
+ if state is None:
177
+ state = _registry[name] = GlobalState()
178
+ return state
File without changes
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: globalstash
3
+ Version: 0.1.0
4
+ Summary: Thread-safe, process-wide global state you can import from any module at any depth.
5
+ Author-email: Your Name <you@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/your-username/globalstash
8
+ Keywords: global,state,flask.g,config,singleton,thread-safe
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # globalstash
20
+
21
+ Thread-safe, process-wide global state you can import from **any module at any depth**, with no `sys.path` tricks.
22
+
23
+ ```bash
24
+ pip install globalstash
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ from globalstash import g
31
+
32
+ # anywhere in your app
33
+ g.db_pool = create_pool()
34
+ g["config"] = {"debug": True}
35
+
36
+ g.get("missing", "default")
37
+ g.setdefault("cache", {})
38
+ g.update(env="prod", version="1.0")
39
+ "db_pool" in g
40
+ del g.db_pool
41
+ ```
42
+
43
+ Because Python caches modules, every `from globalstash import g` returns the same object.
44
+
45
+ ### Atomic and compound operations
46
+
47
+ ```python
48
+ g.incr("requests") # atomic counter
49
+
50
+ with g.lock: # read-modify-write
51
+ g.total = g.total + 1
52
+ ```
53
+
54
+ ### Named namespaces
55
+
56
+ ```python
57
+ from globalstash import get_state
58
+
59
+ cache = get_state("cache") # same name -> same object everywhere
60
+ cache.ttl = 60
61
+ ```
62
+
63
+ ### Tests
64
+
65
+ ```python
66
+ with g.temporary(debug=True):
67
+ ... # values restored on exit
68
+ ```
69
+
70
+ ## Notes
71
+
72
+ - Method names (`get`, `pop`, `update`, ...) can't be used as attribute keys; use `g["get"]`.
73
+ - State lives **per process**. Gunicorn/Uvicorn workers each have their own copy. Use Redis or a database for state shared across processes.
74
+ - For per-request state (like `flask.g`), use `contextvars` instead.
75
+
76
+ ## License
77
+
78
+ MIT
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/globalstash/__init__.py
5
+ src/globalstash/core.py
6
+ src/globalstash/py.typed
7
+ src/globalstash.egg-info/PKG-INFO
8
+ src/globalstash.egg-info/SOURCES.txt
9
+ src/globalstash.egg-info/dependency_links.txt
10
+ src/globalstash.egg-info/top_level.txt
11
+ tests/test_core.py
@@ -0,0 +1 @@
1
+ globalstash
@@ -0,0 +1,88 @@
1
+ import threading
2
+
3
+ import pytest
4
+
5
+ from globalstash import GlobalState, g, get_state
6
+
7
+
8
+ @pytest.fixture(autouse=True)
9
+ def _clean():
10
+ g.clear()
11
+ yield
12
+ g.clear()
13
+
14
+
15
+ def test_attr_and_item_access():
16
+ g.a = 1
17
+ g["b"] = 2
18
+ assert g.a == 1 and g["a"] == 1 and g.b == 2
19
+ assert "a" in g and len(g) == 2
20
+
21
+
22
+ def test_missing_attr_raises():
23
+ with pytest.raises(AttributeError):
24
+ g.nope
25
+
26
+
27
+ def test_get_pop_setdefault_update():
28
+ assert g.get("x", 5) == 5
29
+ assert g.setdefault("x", 1) == 1
30
+ assert g.setdefault("x", 2) == 1
31
+ g.update(y=3, z=4)
32
+ assert g.pop("y") == 3
33
+ assert g.pop("missing", None) is None
34
+ assert g.to_dict() == {"x": 1, "z": 4}
35
+
36
+
37
+ def test_delete():
38
+ g.a = 1
39
+ del g.a
40
+ assert "a" not in g
41
+
42
+
43
+ def test_same_object_across_imports():
44
+ from globalstash import g as g2
45
+ g.v = 1
46
+ assert g2.v == 1
47
+
48
+
49
+ def test_named_registry():
50
+ assert get_state("app") is get_state("app")
51
+ assert get_state("app") is not get_state("other")
52
+
53
+
54
+ def test_temporary():
55
+ g.a = 1
56
+ with g.temporary(a=2, b=3):
57
+ assert g.a == 2 and g.b == 3
58
+ assert g.a == 1 and "b" not in g
59
+
60
+
61
+ def test_incr_is_atomic_across_threads():
62
+ def work():
63
+ for _ in range(2000):
64
+ g.incr("n")
65
+
66
+ threads = [threading.Thread(target=work) for _ in range(8)]
67
+ [t.start() for t in threads]
68
+ [t.join() for t in threads]
69
+ assert g.n == 16000
70
+
71
+
72
+ def test_lock_compound_op():
73
+ g.total = 0
74
+
75
+ def work():
76
+ for _ in range(2000):
77
+ with g.lock:
78
+ g.total = g.total + 1
79
+
80
+ threads = [threading.Thread(target=work) for _ in range(8)]
81
+ [t.start() for t in threads]
82
+ [t.join() for t in threads]
83
+ assert g.total == 16000
84
+
85
+
86
+ def test_independent_instances():
87
+ a, b = GlobalState(x=1), GlobalState()
88
+ assert "x" not in b and a.x == 1