Skip to content

Commit 4399cea

Browse files
committed
Complete rewrite
1 parent b5d8ce4 commit 4399cea

File tree

9 files changed

+365
-104
lines changed

9 files changed

+365
-104
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,6 @@ cython_debug/
169169

170170
# PyPI configuration file
171171
.pypirc
172+
173+
# Misk
174+
.DS_Store

LICENSE

Lines changed: 0 additions & 21 deletions
This file was deleted.

README.md

Lines changed: 77 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,80 @@
1-
# micro env
1+
# microenv
22

3-
Minimal implementation of environment with different data types represented as a tree.
3+
[![PyPI version](https://img.shields.io/pypi/v/microenv.svg)](https://pypi.org/project/microenv/)
4+
[![GitHub release](https://img.shields.io/github/v/release/ceil-python/microenv.svg?logo=github)](https://github.com/ceil-python/microenv/releases)
5+
[![License](https://img.shields.io/pypi/l/supply-demand.svg)](LICENSE)
6+
7+
A minimal Python environment abstraction with privacy controls and async “next” subscriptions.
8+
9+
## Installation
10+
11+
```bash
12+
pip install microenv
13+
```
14+
15+
or
16+
17+
```bash
18+
python -m pip install microenv
19+
```
20+
21+
or
22+
23+
```bash
24+
python3 -m pip install microenv
25+
```
26+
27+
## Quickstart
428

529
```python
6-
from env import MicroEnv
7-
8-
if __name__ == "__main__":
9-
def test():
10-
return 2
11-
micro_env = MicroEnv(
12-
{"prop_a": 1, "prop_b": 2, "prop_c": test},
13-
{
14-
"id": "my_env",
15-
"children": [
16-
{
17-
"prop_d": {"type": "int", "value": 1},
18-
},
19-
{"id": "my_env1"},
20-
],
21-
},
22-
)
23-
micro_env.set("prop_a", 10)
24-
micro_env.get("prop_a")
25-
micro_env.set("prop_d", test, "root/my_env1")
26-
micro_env.get("prop_d", "root/my_env1")
27-
```
30+
import asyncio
31+
from microenv import microenv
32+
33+
# Define initial data and optional descriptor
34+
data = {"public": 1, "secret": "s3cr3t"}
35+
descriptor = {
36+
"children": [
37+
{"key": "public", "type": "number"},
38+
{"key": "secret", "type": "string", "private": True},
39+
]
40+
}
41+
42+
# Create the environment
43+
env = microenv(obj=data, descriptor=descriptor)
44+
face = env.face
45+
46+
# Basic get / set via the face
47+
print(face.public) # → 1
48+
face.public = 42
49+
print(env.data["public"]) # → 42
50+
51+
# Privacy: direct .secret bypasses privacy checks on the face
52+
print(face.secret) # → "s3cr3t"
53+
face.secret = "new!"
54+
print(env.data["secret"]) # → "new!"
55+
56+
# Async “next” subscription: await the next update to a key
57+
async def wait_for_update():
58+
fut = env.get("public", next_=True)
59+
print("waiting for next public…")
60+
val = await fut
61+
print("new public value:", val)
62+
63+
# Schedule waiter and then update
64+
loop = asyncio.get_event_loop()
65+
loop.create_task(wait_for_update())
66+
loop.call_soon(lambda: setattr(face, "public", 99))
67+
loop.run_forever()
68+
```
69+
70+
## API
71+
72+
- `microenv(obj: dict, descriptor: dict = None) → MicroEnv`
73+
- `env.face` : proxy for getting/setting properties.
74+
- `env.get(key, caller=None, next_=False)` : synchronous read or, if `next_=True`, a Future resolving on next `set`.
75+
- `env.set(key, value, caller=None)` : update a property, resolving any pending “next” futures.
76+
77+
## License
78+
79+
This project is licensed under the MIT License.
80+
See [LICENSE](https://github.com/ceil-python/microenv/blob/main/LICENSE) for details.

__init__.py

Whitespace-only changes.

env.py

Lines changed: 0 additions & 59 deletions
This file was deleted.

pyproject.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[project]
2+
name = "microenv"
3+
version = "0.0.4"
4+
authors = [{ name = "Sergey Shkatula", email = "[email protected]" }]
5+
description = "Functional dependency paradigm"
6+
keywords = ["micro", "env", "environment", "microenv"]
7+
requires-python = ">=3.7"
8+
license = "MIT"
9+
10+
[build-system]
11+
requires = ["hatchling >= 1.26"]
12+
build-backend = "hatchling.build"
13+
14+
[tool.hatch.build.targets.wheel]
15+
packages = ["src/MicroEnv"]
16+
17+
[project.urls]
18+
Homepage = "https://github.com/ceil-python/microenv"

src/MicroEnv/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .microenv import microenv
2+
3+
__version__ = "0.0.4"

src/MicroEnv/microenv.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import inspect
2+
3+
try:
4+
import uasyncio as asyncio
5+
except ModuleNotFoundError:
6+
import asyncio
7+
8+
9+
class MicroEnv:
10+
def __init__(
11+
self,
12+
descriptor,
13+
face,
14+
data,
15+
get,
16+
set_,
17+
_awaiters,
18+
_get_awaiter,
19+
_pending_get,
20+
):
21+
self.descriptor = descriptor
22+
self.face = face
23+
self.data = data
24+
self.get = get
25+
self.set = set_
26+
self._awaiters = _awaiters
27+
self._get_awaiter = _get_awaiter
28+
self._pending_get = _pending_get
29+
30+
31+
class Awaiter:
32+
def __init__(self):
33+
self._future = asyncio.get_event_loop().create_future()
34+
35+
def resolve(self, value):
36+
if not self._future.done():
37+
self._future.set_result(value)
38+
39+
def reject(self, reason):
40+
if not self._future.done():
41+
self._future.set_exception(
42+
reason if isinstance(reason, Exception) else Exception(str(reason))
43+
)
44+
45+
@property
46+
def promise(self):
47+
return self._future
48+
49+
def then(self, cb):
50+
self._future.add_done_callback(lambda fut: cb(fut.result()))
51+
52+
53+
def microenv(obj=None, descriptor=None, overrides=None):
54+
obj = obj or {}
55+
descriptor = descriptor or {}
56+
overrides = overrides or {}
57+
58+
def infer_type(v):
59+
if v is None:
60+
return "null"
61+
elif isinstance(v, str):
62+
return "string"
63+
elif isinstance(v, bool):
64+
return "boolean"
65+
elif isinstance(v, (int, float)):
66+
return "number"
67+
elif isinstance(v, list):
68+
return "array"
69+
elif isinstance(v, dict):
70+
return "object"
71+
elif hasattr(v, "__await__"):
72+
return "promise"
73+
else:
74+
return "unknown"
75+
76+
if "children" not in descriptor:
77+
children = [
78+
{
79+
"key": k,
80+
"type": ("unknown" if k not in obj else infer_type(obj[k])),
81+
}
82+
for k in obj
83+
]
84+
descriptor = {
85+
"key": "environment",
86+
"type": "environment",
87+
**descriptor,
88+
"children": children,
89+
}
90+
children_map = {c["key"]: c for c in descriptor.get("children", [])}
91+
_awaiters = {}
92+
_pending_get = {}
93+
94+
def _get_awaiter(key):
95+
if key not in _awaiters:
96+
_awaiters[key] = Awaiter()
97+
return _awaiters[key]
98+
99+
def get(key, caller=None, next_=False):
100+
child_descriptor = children_map.get(key)
101+
if not child_descriptor:
102+
raise KeyError(f'microenv: get non-existent property "{key}"')
103+
# privacy: only matter if the key is private and a caller is supplied
104+
if child_descriptor.get("private") and caller:
105+
raise PermissionError(f'microenv: get private property "{key}"')
106+
# async “next” request
107+
if next_:
108+
return _pending_get.setdefault(key, _get_awaiter(key).promise)
109+
# otherwise just return the current value
110+
return obj.get(key)
111+
112+
def set_(key, value, caller=None):
113+
child_descriptor = children_map.get(key)
114+
if not child_descriptor:
115+
raise KeyError(f'microenv: set non-existent property "{key}"')
116+
if caller and child_descriptor.get("private"):
117+
raise PermissionError(f'microenv: set private property "{key}"')
118+
if key in _awaiters:
119+
_awaiters[key].resolve(value)
120+
obj[key] = value
121+
return value
122+
123+
class Face:
124+
__slots__ = ()
125+
126+
def __getattr__(self, key):
127+
v = get(key)
128+
if callable(v):
129+
return lambda payload, caller=None: v(payload, caller)
130+
return v
131+
132+
def __setattr__(self, key, value):
133+
set_(key, value)
134+
135+
def __getitem__(self, key):
136+
return self.__getattr__(key)
137+
138+
def __setitem__(self, key, value):
139+
self.__setattr__(key, value)
140+
141+
return MicroEnv(
142+
descriptor=descriptor,
143+
face=Face(),
144+
data=obj,
145+
get=get,
146+
set_=set_,
147+
_awaiters=_awaiters,
148+
_get_awaiter=_get_awaiter,
149+
_pending_get=_pending_get,
150+
)

0 commit comments

Comments
 (0)