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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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