kryptic-daemon-client 0.1.0__py3-none-any.whl
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.
- kryptic/__init__.py +202 -0
- kryptic_daemon_client-0.1.0.dist-info/METADATA +56 -0
- kryptic_daemon_client-0.1.0.dist-info/RECORD +6 -0
- kryptic_daemon_client-0.1.0.dist-info/WHEEL +5 -0
- kryptic_daemon_client-0.1.0.dist-info/licenses/LICENSE +21 -0
- kryptic_daemon_client-0.1.0.dist-info/top_level.txt +1 -0
kryptic/__init__.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Kryptic daemon client for Python.
|
|
2
|
+
|
|
3
|
+
During development startup, ``kryptic.inject()`` asks the local Kryptic daemon for
|
|
4
|
+
the current project's secrets and puts them into ``os.environ``. Outside development it
|
|
5
|
+
is a no-op. It never raises - a missing daemon means the application simply starts with
|
|
6
|
+
whatever environment it already has.
|
|
7
|
+
|
|
8
|
+
Protocol: daemon/PROTOCOL.md v1 (newline-delimited JSON over a local socket).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import socket
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
PROTOCOL_VERSION = 1
|
|
23
|
+
|
|
24
|
+
__all__ = ["inject", "InjectResult"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class InjectResult:
|
|
29
|
+
injected: int
|
|
30
|
+
skipped: bool
|
|
31
|
+
reason: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def inject(
|
|
35
|
+
environment: Optional[str] = None,
|
|
36
|
+
project_id: Optional[str] = None,
|
|
37
|
+
timeout_ms: Optional[int] = None,
|
|
38
|
+
) -> InjectResult:
|
|
39
|
+
"""Fetch secrets from the daemon and inject them into ``os.environ``.
|
|
40
|
+
|
|
41
|
+
Existing environment variables are never overwritten. Call before any
|
|
42
|
+
``os.environ`` reads (e.g. at the top of ``manage.py`` for Django).
|
|
43
|
+
"""
|
|
44
|
+
skip_reason = _should_skip()
|
|
45
|
+
if skip_reason:
|
|
46
|
+
return InjectResult(injected=0, skipped=True, reason=skip_reason)
|
|
47
|
+
|
|
48
|
+
config = _find_kryptic_json()
|
|
49
|
+
|
|
50
|
+
project_id = project_id or os.environ.get("KRYPTIC_PROJECT_ID") or (config or {}).get("projectId")
|
|
51
|
+
if not project_id:
|
|
52
|
+
_warn("no kryptic.json found (and no KRYPTIC_PROJECT_ID set) - nothing to inject.")
|
|
53
|
+
return InjectResult(injected=0, skipped=True, reason="no_project")
|
|
54
|
+
|
|
55
|
+
environment = (
|
|
56
|
+
environment
|
|
57
|
+
or os.environ.get("KRYPTIC_ENV")
|
|
58
|
+
or (config or {}).get("defaultEnvironment")
|
|
59
|
+
or "development"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
timeout = (timeout_ms or int(os.environ.get("KRYPTIC_TIMEOUT_MS", "2000"))) / 1000.0
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
response = _request(
|
|
66
|
+
{"v": PROTOCOL_VERSION, "type": "secrets", "projectId": project_id, "environment": environment},
|
|
67
|
+
timeout,
|
|
68
|
+
)
|
|
69
|
+
except OSError as e:
|
|
70
|
+
_warn(f"daemon not reachable ({e}) - continuing without injected secrets.")
|
|
71
|
+
return InjectResult(injected=0, skipped=True, reason="daemon_unreachable")
|
|
72
|
+
except ValueError:
|
|
73
|
+
_warn("daemon sent an invalid response - continuing without injected secrets.")
|
|
74
|
+
return InjectResult(injected=0, skipped=True, reason="invalid_response")
|
|
75
|
+
|
|
76
|
+
if not response.get("ok"):
|
|
77
|
+
error = response.get("error", "internal")
|
|
78
|
+
_warn(f"daemon refused the request ({error}): {response.get('message', '')}")
|
|
79
|
+
return InjectResult(injected=0, skipped=True, reason=error)
|
|
80
|
+
|
|
81
|
+
injected = 0
|
|
82
|
+
for secret in response.get("secrets", []):
|
|
83
|
+
key = secret.get("key")
|
|
84
|
+
if not key or key in os.environ: # real environment always wins
|
|
85
|
+
continue
|
|
86
|
+
os.environ[key] = secret.get("value", "")
|
|
87
|
+
injected += 1
|
|
88
|
+
|
|
89
|
+
return InjectResult(injected=injected, skipped=False)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------- internals ----------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _should_skip() -> Optional[str]:
|
|
96
|
+
if os.environ.get("KRYPTIC_DISABLED") == "true":
|
|
97
|
+
return "disabled"
|
|
98
|
+
|
|
99
|
+
# Python has no single convention; honor the common ones.
|
|
100
|
+
for variable in ("ENVIRONMENT", "ENV", "PYTHON_ENV", "APP_ENV"):
|
|
101
|
+
value = os.environ.get(variable, "").lower()
|
|
102
|
+
if value in ("production", "prod", "staging"):
|
|
103
|
+
return f"{variable.lower()}_{value}"
|
|
104
|
+
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _socket_path() -> str:
|
|
109
|
+
override = os.environ.get("KRYPTIC_SOCKET_PATH")
|
|
110
|
+
if override:
|
|
111
|
+
return override
|
|
112
|
+
|
|
113
|
+
if sys.platform == "win32":
|
|
114
|
+
return r"\\.\pipe\kryptic-daemon"
|
|
115
|
+
|
|
116
|
+
if sys.platform == "linux":
|
|
117
|
+
runtime_dir = os.environ.get("XDG_RUNTIME_DIR")
|
|
118
|
+
if runtime_dir:
|
|
119
|
+
return str(Path(runtime_dir) / "kryptic-daemon.sock")
|
|
120
|
+
|
|
121
|
+
return "/tmp/kryptic-daemon.sock"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _request(payload: dict, timeout: float) -> dict:
|
|
125
|
+
line = (json.dumps(payload) + "\n").encode("utf-8")
|
|
126
|
+
|
|
127
|
+
if sys.platform == "win32" and _socket_path().startswith("\\\\.\\pipe\\"):
|
|
128
|
+
raw = _round_trip_named_pipe(line, timeout)
|
|
129
|
+
else:
|
|
130
|
+
raw = _round_trip_unix_socket(line, timeout)
|
|
131
|
+
|
|
132
|
+
return json.loads(raw.decode("utf-8"))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _round_trip_unix_socket(line: bytes, timeout: float) -> bytes:
|
|
136
|
+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
|
|
137
|
+
client.settimeout(timeout)
|
|
138
|
+
client.connect(_socket_path())
|
|
139
|
+
client.sendall(line)
|
|
140
|
+
|
|
141
|
+
buffer = b""
|
|
142
|
+
while b"\n" not in buffer:
|
|
143
|
+
chunk = client.recv(4096)
|
|
144
|
+
if not chunk:
|
|
145
|
+
raise OSError("connection closed")
|
|
146
|
+
buffer += chunk
|
|
147
|
+
|
|
148
|
+
return buffer.split(b"\n", 1)[0]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _round_trip_named_pipe(line: bytes, timeout: float) -> bytes:
|
|
152
|
+
"""Round trip over a Windows named pipe.
|
|
153
|
+
|
|
154
|
+
The daemon serves a byte-mode pipe, so a plain file handle works - no win32
|
|
155
|
+
bindings needed. Mirrors the .NET client: the timeout covers connecting (the
|
|
156
|
+
pipe may briefly report "busy" between served clients); the read then blocks
|
|
157
|
+
until the daemon replies, which it does immediately or not at all.
|
|
158
|
+
"""
|
|
159
|
+
deadline = time.monotonic() + timeout
|
|
160
|
+
while True:
|
|
161
|
+
try:
|
|
162
|
+
pipe = open(_socket_path(), "r+b", buffering=0) # noqa: SIM115 - closed below
|
|
163
|
+
break
|
|
164
|
+
except OSError:
|
|
165
|
+
if time.monotonic() >= deadline:
|
|
166
|
+
raise OSError("timed out connecting to the daemon pipe") from None
|
|
167
|
+
time.sleep(0.05)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
pipe.write(line)
|
|
171
|
+
|
|
172
|
+
buffer = b""
|
|
173
|
+
while b"\n" not in buffer:
|
|
174
|
+
chunk = pipe.read(4096)
|
|
175
|
+
if not chunk:
|
|
176
|
+
raise OSError("connection closed")
|
|
177
|
+
buffer += chunk
|
|
178
|
+
|
|
179
|
+
return buffer.split(b"\n", 1)[0]
|
|
180
|
+
finally:
|
|
181
|
+
pipe.close()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _find_kryptic_json() -> Optional[dict]:
|
|
185
|
+
directory = Path.cwd()
|
|
186
|
+
while True:
|
|
187
|
+
candidate = directory / "kryptic.json"
|
|
188
|
+
if candidate.is_file():
|
|
189
|
+
try:
|
|
190
|
+
return json.loads(candidate.read_text(encoding="utf-8"))
|
|
191
|
+
except ValueError:
|
|
192
|
+
_warn(f"could not parse {candidate} - ignoring it.")
|
|
193
|
+
return None
|
|
194
|
+
if directory.parent == directory:
|
|
195
|
+
return None
|
|
196
|
+
directory = directory.parent
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _warn(message: str) -> None:
|
|
200
|
+
if os.environ.get("KRYPTIC_SILENT") == "true":
|
|
201
|
+
return
|
|
202
|
+
print(f"[kryptic] {message}", file=sys.stderr)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kryptic-daemon-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Kryptic daemon client for Python. Passively injects development secrets from the local Kryptic daemon into os.environ. No-op outside development.
|
|
5
|
+
Author: Kryptic
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://kryptic.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/dev-kryptic/Kryptic.Python
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# kryptic-daemon-client
|
|
15
|
+
|
|
16
|
+
The Kryptic daemon client for Python. During development startup it asks the local
|
|
17
|
+
Kryptic daemon for the current project's secrets and puts them into `os.environ`.
|
|
18
|
+
Outside development it is a no-op. It never raises - no daemon just means your app
|
|
19
|
+
starts with the environment it already has.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install kryptic-daemon-client
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import kryptic
|
|
27
|
+
kryptic.inject() # call before any os.environ reads
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
db_url = os.environ["DATABASE_URL"] # now populated
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Django (`manage.py`, before `django.setup()`):
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import kryptic
|
|
37
|
+
kryptic.inject()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Works with Django, FastAPI, Flask, and anything else that reads `os.environ`.
|
|
41
|
+
|
|
42
|
+
## Behavior
|
|
43
|
+
|
|
44
|
+
- No-op when `ENVIRONMENT`/`ENV`/`PYTHON_ENV`/`APP_ENV` is production/staging,
|
|
45
|
+
or `KRYPTIC_DISABLED=true`.
|
|
46
|
+
- Finds `kryptic.json` by walking up from the working directory.
|
|
47
|
+
- Never overwrites environment variables that are already set.
|
|
48
|
+
- Configuration via env vars: `KRYPTIC_PROJECT_ID`, `KRYPTIC_ENV`, `KRYPTIC_SOCKET_PATH`,
|
|
49
|
+
`KRYPTIC_TIMEOUT_MS` (default 2000), `KRYPTIC_SILENT`.
|
|
50
|
+
- Works on macOS/Linux (unix sockets) and Windows (named pipes).
|
|
51
|
+
|
|
52
|
+
Protocol: see [daemon/PROTOCOL.md](../../Kryptic.Daemon/PROTOCOL.md). License: MIT.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python3 -m unittest discover -s tests
|
|
56
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
kryptic/__init__.py,sha256=5andTxuKswb_tV8oUYuPo6q9FQOCu4ErfuXgTEZ2kdM,6427
|
|
2
|
+
kryptic_daemon_client-0.1.0.dist-info/licenses/LICENSE,sha256=hAdyWC0j7HF2Ejq2ENoOCKYYVHUadFo0inm0bVbefNE,1096
|
|
3
|
+
kryptic_daemon_client-0.1.0.dist-info/METADATA,sha256=RYvDTpykC4jAUlA5nPIvpF37jmV6wMxNvKdCnP5s2Bw,1755
|
|
4
|
+
kryptic_daemon_client-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
kryptic_daemon_client-0.1.0.dist-info/top_level.txt,sha256=DZK06vKfekJE1xYTYheRUCBhuM_iwA4b9sEinDkW8ew,8
|
|
6
|
+
kryptic_daemon_client-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kryptic by Theka.dev SINGLE MEMBER P.C.
|
|
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 @@
|
|
|
1
|
+
kryptic
|