langfuse-hotswap 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.
Files changed (30) hide show
  1. langfuse_hotswap-0.1.0/.claude/rules/config-security.md +22 -0
  2. langfuse_hotswap-0.1.0/.claude/rules/docs-and-ci.md +29 -0
  3. langfuse_hotswap-0.1.0/.claude/rules/python-style.md +35 -0
  4. langfuse_hotswap-0.1.0/.claude/rules/runnable.md +22 -0
  5. langfuse_hotswap-0.1.0/.claude/rules/tests.md +22 -0
  6. langfuse_hotswap-0.1.0/.github/workflows/ci.yml +29 -0
  7. langfuse_hotswap-0.1.0/.github/workflows/release.yml +42 -0
  8. langfuse_hotswap-0.1.0/.gitignore +23 -0
  9. langfuse_hotswap-0.1.0/.python-version +1 -0
  10. langfuse_hotswap-0.1.0/CLAUDE.md +48 -0
  11. langfuse_hotswap-0.1.0/LICENSE +21 -0
  12. langfuse_hotswap-0.1.0/PKG-INFO +404 -0
  13. langfuse_hotswap-0.1.0/README.md +378 -0
  14. langfuse_hotswap-0.1.0/pyproject.toml +133 -0
  15. langfuse_hotswap-0.1.0/src/langfuse_hotswap/__init__.py +10 -0
  16. langfuse_hotswap-0.1.0/src/langfuse_hotswap/_config.py +170 -0
  17. langfuse_hotswap-0.1.0/src/langfuse_hotswap/_errors.py +21 -0
  18. langfuse_hotswap-0.1.0/src/langfuse_hotswap/_factory.py +88 -0
  19. langfuse_hotswap-0.1.0/src/langfuse_hotswap/_runnable.py +512 -0
  20. langfuse_hotswap-0.1.0/src/langfuse_hotswap/_template.py +105 -0
  21. langfuse_hotswap-0.1.0/src/langfuse_hotswap/_version.py +24 -0
  22. langfuse_hotswap-0.1.0/src/langfuse_hotswap/py.typed +0 -0
  23. langfuse_hotswap-0.1.0/tests/__init__.py +0 -0
  24. langfuse_hotswap-0.1.0/tests/conftest.py +52 -0
  25. langfuse_hotswap-0.1.0/tests/fakes.py +159 -0
  26. langfuse_hotswap-0.1.0/tests/test_config.py +357 -0
  27. langfuse_hotswap-0.1.0/tests/test_factory.py +137 -0
  28. langfuse_hotswap-0.1.0/tests/test_runnable.py +907 -0
  29. langfuse_hotswap-0.1.0/tests/test_template.py +189 -0
  30. langfuse_hotswap-0.1.0/uv.lock +1676 -0
@@ -0,0 +1,22 @@
1
+ ---
2
+ paths:
3
+ - "src/langfuse_hotswap/_config.py"
4
+ - "src/langfuse_hotswap/_factory.py"
5
+ ---
6
+
7
+ # Model parameters and their security
8
+
9
+ Anyone who can edit a prompt in Langfuse controls its config. These rules keep that from
10
+ becoming control over endpoints, credentials or cost.
11
+
12
+ - Precedence is fixed: prompt config < factory kwargs < provider kwargs. Never let the config
13
+ override something set in code.
14
+ - The provider comes only from an explicit `model_provider`. Do not infer it from the model name
15
+ and do not parse a `provider:model` prefix. The model name is passed as is.
16
+ - With `providers` set, the config may select only one of those providers.
17
+ - `DEFAULT_ALLOWED_CONFIG_KEYS` holds sampling parameters only. Infrastructure (timeouts,
18
+ retries, endpoints, clients, credentials) never goes there.
19
+ - `is_blocked_config_key` is the second line of defense and applies even with
20
+ `allowed_config_keys=None`. When adding a provider-specific infrastructure argument, block it by
21
+ exact name or by a prefix/suffix rule, and add it to the blocked-keys test in `test_config.py`.
22
+ - `build_model_params` stays a pure function: no I/O, no logging, no model construction.
@@ -0,0 +1,29 @@
1
+ ---
2
+ paths:
3
+ - "README.md"
4
+ - "CLAUDE.md"
5
+ - ".claude/rules/**"
6
+ - ".github/workflows/**"
7
+ ---
8
+
9
+ # Documentation and CI
10
+
11
+ ## README
12
+
13
+ - The README is the user documentation and holds the rationale that docstrings leave out.
14
+ - A change in behavior, defaults or the public API updates the README in the same change.
15
+ - Code samples in the README must run against the current API.
16
+
17
+ ## CLAUDE.md and rules
18
+
19
+ - Written in English, the whole file.
20
+ - `CLAUDE.md` stays short: commands, layout, invariants. Detailed rules go to `.claude/rules/`
21
+ with a `paths` front matter that limits them to the files they are about.
22
+
23
+ ## GitHub Actions
24
+
25
+ - Pin every action to a full commit SHA with the version in a comment.
26
+ - Default `permissions: contents: read`; grant more per job only where needed.
27
+ - `actions/checkout` uses `persist-credentials: false`.
28
+ - The release builds and tests in a job without `id-token`; only the publish job gets
29
+ `id-token: write`, and it runs no project code.
@@ -0,0 +1,35 @@
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ ---
5
+
6
+ # Python style
7
+
8
+ ## Docstrings
9
+
10
+ - Every docstring is multi-line: `"""` on its own line, the text, `"""` on its own line. No one-line form.
11
+ - In a function or method, one blank line separates the docstring from the code that follows.
12
+ - A docstring says what the object does or returns, in one or two sentences. Behavior that the
13
+ caller must know (precedence, defaults, what is raised) may follow as a short second paragraph.
14
+ - No reasoning, consequences or motivation ("so that", "because", "this means"). The rationale
15
+ belongs in the README or, when it is local and non-obvious, in a one-line code comment.
16
+ - Methods that implement the `Runnable` interface as is (`invoke`, `stream`, …) have no docstring.
17
+
18
+ ## Layout of code
19
+
20
+ - Separate the logical steps of a function with one blank line: validation, computation, the
21
+ result. Do not separate lines that form one step.
22
+ - A predicate used in a condition gets its own method or function when it has more than two
23
+ clauses (see `_cache_is_warm`).
24
+ - Group a module as: module docstring, imports, type variables, logger, private helper types,
25
+ public classes, module-level helper functions.
26
+ - Pure logic that does not need the runnable state belongs in its own module (see `_config.py`).
27
+
28
+ ## Code
29
+
30
+ - No `__all__`. The public API is whatever `__init__.py` imports.
31
+ - Do not extract a single string or number into a module-level constant; inline it.
32
+ - Prefer `NamedTuple` with named fields over bare tuples that are indexed.
33
+ - `from __future__ import annotations` in every module; imports used only in annotations go under
34
+ `if TYPE_CHECKING:`.
35
+ - Line length is 120, ruff formats and lints, `ty` type-checks. Fix the code, not the config.
@@ -0,0 +1,22 @@
1
+ ---
2
+ paths:
3
+ - "src/langfuse_hotswap/_runnable.py"
4
+ ---
5
+
6
+ # The runnable
7
+
8
+ - Every `Runnable` method resolves the chain once (`_prepare` or `_aprepare`) and delegates to it.
9
+ A batch method fetches the prompt once for all inputs.
10
+ - `_built` and `_failed` are replaced as whole `NamedTuple`s, never mutated, so readers outside the
11
+ lock see a consistent pair.
12
+ - Rebuilds run under `_lock`. Without a built chain callers wait for the build; with one, a call
13
+ that finds the lock taken is served by the previous chain.
14
+ - A failed build is cached for a minute in `_failed`; a failed fetch is logged at most once a
15
+ minute. Both keep serving the previous chain.
16
+ - Async: fetch on the event loop only when `_cache_is_warm()` is true; otherwise, and for every
17
+ rebuild, go through `run_in_executor`. Never call a blocking fetch or build directly in an
18
+ `async def`.
19
+ - Hooks and the template metadata get a deep copy of the prompt; the original is kept only for
20
+ comparison. Compare prompts by identity first, then by content.
21
+ - Errors leaving the runnable are `PromptFetchError` or `ChainBuildError` with the original as
22
+ `__cause__`.
@@ -0,0 +1,22 @@
1
+ ---
2
+ paths:
3
+ - "tests/**/*.py"
4
+ ---
5
+
6
+ # Tests
7
+
8
+ - One test module per source module: `test_config.py`, `test_runnable.py`, `test_template.py`,
9
+ `test_factory.py`. Inside a module, group tests under `# Section` comments.
10
+ - Every test has a docstring with one sentence that states the behavior under test, in the
11
+ present tense: "A version that fails to build is served by the previous chain and logged once."
12
+ Not "Tests that…", not the implementation.
13
+ - The test name and the docstring describe the same behavior; rename the test when it changes.
14
+ - Arrange, act and assert are separated by blank lines.
15
+ - Use the fixtures from `conftest.py`: `client` (fake Langfuse), `init_calls` (records the
16
+ `init_chat_model` arguments and returns `FakeChatModel`), `factory` (allowlist disabled).
17
+ - Shared doubles live in `fakes.py`. Do not add network calls or real providers.
18
+ - Time-dependent behavior (the one-minute windows) is tested by patching
19
+ `langfuse_hotswap._runnable.time.monotonic`, not by sleeping.
20
+ - Concurrency tests may slow a hook down with a short `time.sleep` so that concurrent calls
21
+ overlap. Use `threading.Event` with a timeout when the test needs an exact interleaving.
22
+ - A security rule (blocked keys, provider selection, precedence) gets a test that tries to break it.
@@ -0,0 +1,29 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ python-version: ["3.11", "3.12", "3.13"]
17
+ steps:
18
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
19
+ with:
20
+ fetch-depth: 0
21
+ persist-credentials: false
22
+ - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+ - run: uv sync --locked
26
+ - run: uv run ruff check .
27
+ - run: uv run ruff format --check .
28
+ - run: uv run ty check
29
+ - run: uv run pytest -q
@@ -0,0 +1,42 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ # Runs the tests and builds the package without publishing rights, so code from the dev
12
+ # dependencies never sees the PyPI token.
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
17
+ with:
18
+ fetch-depth: 0
19
+ persist-credentials: false
20
+ - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
21
+ - run: uv sync --locked
22
+ - run: uv run pytest -q
23
+ - run: uv build
24
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
25
+ with:
26
+ name: dist
27
+ path: dist/
28
+ if-no-files-found: error
29
+
30
+ publish:
31
+ needs: build
32
+ runs-on: ubuntu-latest
33
+ environment: pypi
34
+ permissions:
35
+ id-token: write
36
+ steps:
37
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+ - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
42
+ - run: uv publish --trusted-publishing always dist/*
@@ -0,0 +1,23 @@
1
+ # Mac OS
2
+ .DS_Store
3
+
4
+ # JetBrains
5
+ .idea
6
+
7
+ # Byte-compiled / optimized / DLL files
8
+ __pycache__/
9
+ *.py[codz]
10
+
11
+ # Distribution / packaging
12
+ src/langfuse_hotswap/_version.py
13
+ build/
14
+ dist/
15
+ *.egg-info/
16
+
17
+ # Environments and secrets
18
+ .venv/
19
+ .env
20
+
21
+ # Tooling caches
22
+ .pytest_cache/
23
+ .ruff_cache/
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,48 @@
1
+ # langfuse-hotswap
2
+
3
+ A LangChain `Runnable` whose chat model and its parameters live in the Langfuse prompt config.
4
+ Every call fetches the prompt through the Langfuse SDK cache and delegates to `template | model`
5
+ built for it; the chain is rebuilt when the prompt changes.
6
+
7
+ ## Commands
8
+
9
+ ```shell
10
+ uv sync # install with the dev group
11
+ uv run pytest -q # tests
12
+ uv run ruff format . && uv run ruff check . && uv run ty check
13
+ ```
14
+
15
+ Run all four before reporting a change as done. CI runs the same on Python 3.11–3.13.
16
+
17
+ ## Layout
18
+
19
+ | Path | What lives there |
20
+ |---|---|
21
+ | `src/langfuse_hotswap/_config.py` | `build_model_params`: allowlist, blocked keys, explicit provider, precedence of kwargs |
22
+ | `src/langfuse_hotswap/_runnable.py` | `LangfusePromptRunnable`: fetch, compare, rebuild under a lock, async fast path, hooks |
23
+ | `src/langfuse_hotswap/_template.py` | `LangfusePromptTemplate`: rendering with the Langfuse `TemplateParser` |
24
+ | `src/langfuse_hotswap/_factory.py` | `LangfuseChainFactory`: shared client and kwargs, `create` arguments |
25
+ | `src/langfuse_hotswap/_errors.py` | `HotswapError`, `PromptFetchError`, `ChainBuildError` |
26
+ | `tests/fakes.py`, `tests/conftest.py` | fake Langfuse client, fake chat model, `init_calls` fixture |
27
+ | `tests/test_<module>.py` | one test module per source module |
28
+
29
+ The public API is what `src/langfuse_hotswap/__init__.py` imports. Modules are private (`_name.py`).
30
+
31
+ ## Invariants
32
+
33
+ - Code beats config: precedence is prompt config < factory kwargs < provider kwargs.
34
+ - The provider comes only from an explicit `model_provider`; it is never inferred from the model name.
35
+ - Blocked config keys (`is_blocked_config_key`) never reach the model, whatever the allowlist says.
36
+ - A broken prompt never takes down a working chain: the previous chain keeps serving.
37
+ - The async path never blocks the event loop on HTTP when the SDK cache is cold.
38
+
39
+ Behavior changes go together with tests and the README.
40
+
41
+ ## Conventions
42
+
43
+ Path-scoped rules in `.claude/rules/` load when matching files are touched: Python style, tests,
44
+ the config and runnable modules, README and CI. The short version:
45
+
46
+ - Docstrings say what, not why, and are always multi-line.
47
+ - No `__all__`, no module-level constants for single literals.
48
+ - Never commit or create branches unless asked.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruslan Glotov
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.