instructvault 0.2.1__py3-none-any.whl → 0.2.3__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.
instructvault/bundle.py CHANGED
@@ -29,22 +29,32 @@ def collect_prompts(repo_root: Path, prompts_dir: Path, ref: Optional[str]) -> L
29
29
  store = PromptStore(repo_root)
30
30
  prompts: List[BundlePrompt] = []
31
31
  if ref is None:
32
+ if not prompts_dir.exists():
33
+ raise FileNotFoundError(f"Prompts directory not found: {prompts_dir}")
34
+ try:
35
+ prompts_dir.relative_to(repo_root)
36
+ except Exception:
37
+ raise ValueError("prompts_dir must be within repo_root")
32
38
  for p in sorted(prompts_dir.rglob("*.prompt.y*ml")):
33
39
  rel_path = p.relative_to(repo_root).as_posix()
34
- spec = load_prompt_spec(p.read_text(encoding="utf-8"))
40
+ spec = load_prompt_spec(p.read_text(encoding="utf-8"), allow_no_tests=True)
35
41
  prompts.append(BundlePrompt(rel_path, spec))
36
42
  for p in sorted(prompts_dir.rglob("*.prompt.json")):
37
43
  rel_path = p.relative_to(repo_root).as_posix()
38
- spec = load_prompt_spec(p.read_text(encoding="utf-8"))
44
+ spec = load_prompt_spec(p.read_text(encoding="utf-8"), allow_no_tests=True)
39
45
  prompts.append(BundlePrompt(rel_path, spec))
46
+ if not prompts:
47
+ raise ValueError(f"No prompt files found in {prompts_dir}")
40
48
  return prompts
41
49
 
42
50
  rel_dir = prompts_dir.relative_to(repo_root).as_posix()
43
51
  for rel_path in _list_files_at_ref(repo_root, ref, rel_dir):
44
52
  if not _is_prompt_file(rel_path):
45
53
  continue
46
- spec = load_prompt_spec(store.read_text(rel_path, ref=ref))
54
+ spec = load_prompt_spec(store.read_text(rel_path, ref=ref), allow_no_tests=True)
47
55
  prompts.append(BundlePrompt(rel_path, spec))
56
+ if not prompts:
57
+ raise ValueError(f"No prompt files found at ref {ref} in {rel_dir}")
48
58
  return prompts
49
59
 
50
60
  def write_bundle(out_path: Path, *, repo_root: Path, prompts_dir: Path, ref: Optional[str]) -> None:
@@ -59,4 +69,3 @@ def write_bundle(out_path: Path, *, repo_root: Path, prompts_dir: Path, ref: Opt
59
69
  }
60
70
  out_path.parent.mkdir(parents=True, exist_ok=True)
61
71
  out_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
62
-
instructvault/cli.py CHANGED
@@ -39,7 +39,7 @@ def validate(path: Path = typer.Argument(...),
39
39
  results = []
40
40
  for f in files:
41
41
  try:
42
- spec = load_prompt_spec(f.read_text(encoding="utf-8"))
42
+ spec = load_prompt_spec(f.read_text(encoding="utf-8"), allow_no_tests=False)
43
43
  try:
44
44
  rel_path = f.relative_to(repo).as_posix()
45
45
  except ValueError:
@@ -65,10 +65,14 @@ def render(prompt_path: str = typer.Argument(...),
65
65
  vars_json: str = typer.Option("{}", "--vars"),
66
66
  ref: Optional[str] = typer.Option(None, "--ref"),
67
67
  repo: Path = typer.Option(Path("."), "--repo"),
68
- json_out: bool = typer.Option(False, "--json")):
68
+ json_out: bool = typer.Option(False, "--json"),
69
+ allow_no_tests: bool = typer.Option(False, "--allow-no-tests")):
69
70
  store = PromptStore(repo_root=repo)
70
- spec = load_prompt_spec(store.read_text(prompt_path, ref=ref))
71
- vars_dict = json.loads(vars_json)
71
+ spec = load_prompt_spec(store.read_text(prompt_path, ref=ref), allow_no_tests=allow_no_tests)
72
+ try:
73
+ vars_dict = json.loads(vars_json)
74
+ except Exception:
75
+ raise typer.BadParameter("Invalid JSON for --vars")
72
76
  check_required_vars(spec, vars_dict)
73
77
  msgs = render_messages(spec, vars_dict)
74
78
  if json_out:
@@ -121,7 +125,7 @@ def eval(prompt_path: str = typer.Argument(...),
121
125
  repo: Path = typer.Option(Path("."), "--repo"),
122
126
  json_out: bool = typer.Option(False, "--json")):
123
127
  store = PromptStore(repo_root=repo)
124
- spec = load_prompt_spec(store.read_text(prompt_path, ref=ref))
128
+ spec = load_prompt_spec(store.read_text(prompt_path, ref=ref), allow_no_tests=False)
125
129
 
126
130
  ok1, r1 = run_inline_tests(spec)
127
131
  results = list(r1)
instructvault/io.py CHANGED
@@ -4,13 +4,16 @@ import json
4
4
  import yaml
5
5
  from .spec import DatasetRow, PromptSpec
6
6
 
7
- def load_prompt_spec(yaml_text: str) -> PromptSpec:
7
+ def load_prompt_spec(yaml_text: str, *, allow_no_tests: bool = True) -> PromptSpec:
8
8
  text = yaml_text.strip()
9
9
  if text.startswith("{") or text.startswith("["):
10
- data: Dict[str, Any] = json.loads(text) if text else {}
10
+ try:
11
+ data: Dict[str, Any] = json.loads(text) if text else {}
12
+ except Exception:
13
+ data = yaml.safe_load(yaml_text) or {}
11
14
  else:
12
15
  data = yaml.safe_load(yaml_text) or {}
13
- return PromptSpec.model_validate(data)
16
+ return PromptSpec.model_validate(data, context={"allow_no_tests": allow_no_tests})
14
17
 
15
18
  def load_dataset_jsonl(text: str) -> List[DatasetRow]:
16
19
  rows: List[DatasetRow] = []
instructvault/sdk.py CHANGED
@@ -19,12 +19,14 @@ class InstructVault:
19
19
 
20
20
  def load_prompt(self, prompt_path: str, ref: Optional[str] = None) -> PromptSpec:
21
21
  if self.bundle is not None:
22
+ if ref is not None:
23
+ raise ValueError("ref is not supported when using bundle_path")
22
24
  if prompt_path not in self.bundle:
23
25
  raise FileNotFoundError(f"Prompt not found in bundle: {prompt_path}")
24
26
  return self.bundle[prompt_path]
25
27
  if self.store is None:
26
28
  raise ValueError("No repo_root configured")
27
- return load_prompt_spec(self.store.read_text(prompt_path, ref=ref))
29
+ return load_prompt_spec(self.store.read_text(prompt_path, ref=ref), allow_no_tests=True)
28
30
  def render(self, prompt_path: str, vars: Dict[str, Any], ref: Optional[str] = None) -> List[PromptMessage]:
29
31
  spec = self.load_prompt(prompt_path, ref=ref)
30
32
  check_required_vars(spec, vars)
instructvault/spec.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
  from typing import Any, Dict, List, Literal, Optional
3
- from pydantic import BaseModel, ConfigDict, Field
3
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
4
4
 
5
5
  Role = Literal["system", "user", "assistant", "tool"]
6
6
 
@@ -25,6 +25,11 @@ class AssertSpec(BaseModel):
25
25
  contains_any: Optional[List[str]] = None
26
26
  contains_all: Optional[List[str]] = None
27
27
  not_contains: Optional[List[str]] = None
28
+ @model_validator(mode="after")
29
+ def _require_one(self) -> "AssertSpec":
30
+ if not (self.contains_any or self.contains_all or self.not_contains):
31
+ raise ValueError("assert must include at least one of contains_any, contains_all, not_contains")
32
+ return self
28
33
 
29
34
  class PromptTest(BaseModel):
30
35
  model_config = ConfigDict(extra="forbid")
@@ -42,6 +47,17 @@ class PromptSpec(BaseModel):
42
47
  messages: List[PromptMessage]
43
48
  tests: List[PromptTest] = Field(default_factory=list)
44
49
 
50
+ @model_validator(mode="after")
51
+ def _require_tests(self) -> "PromptSpec":
52
+ allow_no_tests = False
53
+ try:
54
+ allow_no_tests = bool(self.__pydantic_context__.get("allow_no_tests"))
55
+ except Exception:
56
+ allow_no_tests = False
57
+ if not self.tests and not allow_no_tests:
58
+ raise ValueError("prompt must include at least one test")
59
+ return self
60
+
45
61
  class DatasetRow(BaseModel):
46
62
  model_config = ConfigDict(extra="forbid")
47
63
  vars: Dict[str, Any] = Field(default_factory=dict)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: instructvault
3
- Version: 0.2.1
3
+ Version: 0.2.3
4
4
  Summary: Git-first prompt registry + CI evals + lightweight runtime SDK (ivault).
5
5
  Project-URL: Homepage, https://github.com/05satyam/instruct_vault
6
6
  Project-URL: Repository, https://github.com/05satyam/instruct_vault
@@ -14,6 +14,7 @@ Requires-Dist: pyyaml>=6.0
14
14
  Requires-Dist: rich>=13.7
15
15
  Requires-Dist: typer>=0.12
16
16
  Provides-Extra: dev
17
+ Requires-Dist: httpx>=0.27; extra == 'dev'
17
18
  Requires-Dist: mypy>=1.10; extra == 'dev'
18
19
  Requires-Dist: pytest-cov>=5.0; extra == 'dev'
19
20
  Requires-Dist: pytest>=8.0; extra == 'dev'
@@ -48,6 +49,10 @@ flowchart LR
48
49
  Enterprises already have Git + PR reviews + CI/CD. Prompts usually don’t.
49
50
  InstructVault brings **prompt‑as‑code** without requiring a server, database, or platform.
50
51
 
52
+ ## Vision
53
+ Short version: Git‑first prompts with CI governance and zero‑latency runtime.
54
+ Full vision: `docs/vision.md`
55
+
51
56
  ## Features
52
57
  - ✅ Git‑native versioning (tags/SHAs = releases)
53
58
  - ✅ CLI‑first (`init`, `validate`, `render`, `eval`, `diff`, `resolve`, `bundle`)
@@ -128,6 +133,10 @@ ivault render prompts/support_reply.prompt.yml --vars '{"ticket_text":"My app cr
128
133
  ivault eval prompts/support_reply.prompt.yml --dataset datasets/support_cases.jsonl --report out/report.json --junit out/junit.xml
129
134
  ```
130
135
 
136
+ Note: Prompts must include at least one inline test. Datasets are optional.
137
+ Migration tip: if you need to render a prompt that doesn’t yet include tests, use
138
+ `ivault render --allow-no-tests` or add a minimal test first.
139
+
131
140
  ### 5) Version prompts with tags
132
141
  ```bash
133
142
  git add prompts datasets
@@ -176,7 +185,17 @@ export IVAULT_REPO_ROOT=/path/to/your/repo
176
185
  PYTHONPATH=. uvicorn ivault_playground.app:app --reload
177
186
  ```
178
187
 
188
+ ![Playground screenshot](docs/assets/playground.png)
189
+
190
+ Optional auth:
191
+ ```bash
192
+ export IVAULT_PLAYGROUND_API_KEY=your-secret
193
+ ```
194
+ Then send `x-ivault-api-key` in requests (or keep it behind your org gateway).
195
+ If you don’t set the env var, no auth is required.
196
+
179
197
  ## Docs
198
+ - `docs/vision.md`
180
199
  - `docs/governance.md`
181
200
  - `docs/ci.md`
182
201
  - `docs/playground.md`
@@ -0,0 +1,17 @@
1
+ instructvault/__init__.py,sha256=cg7j0qh6W84D-K0uSOLKKAP2JquW4NRXwZRDDLk5E18,59
2
+ instructvault/bundle.py,sha256=6bfHNxJsE3zuZBLX5ZiMAhn1Dw6BnFHRa55fN6XIPRI,3008
3
+ instructvault/cli.py,sha256=v5vP-sgVpXRs-YGvxH8VWIarFqUD1IsXdB9lseaFJDA,6310
4
+ instructvault/diff.py,sha256=vz_vmKDXasNFoVKHCk2u_TsboHk1BdwvX0wCnJI1ATQ,252
5
+ instructvault/eval.py,sha256=-yrFHCEUrONvzfKLP8s_RktFU74Ergp9tQJvzfrMR9s,1949
6
+ instructvault/io.py,sha256=n1yQfiy93Duz-8tJ_HpbCEq8MUn2jlLpSmUY6XBg8G4,1037
7
+ instructvault/junit.py,sha256=sIEcIiGD3Xk6uCYjnE5p_07j8dPoS_RAc2eoy3BIBeQ,1133
8
+ instructvault/render.py,sha256=vcVnqIXGytskZEKbUofoKgIVflQSYhsmdpEtZs1X19A,919
9
+ instructvault/scaffold.py,sha256=f5gwXE3dUPuJYTedZRqBs8w5SQEgt1dgDSuqW2dxrMg,1685
10
+ instructvault/sdk.py,sha256=abqFrmc9Q5LUqC_ZrwM12DlpTZZkXqRuzN0T2x9lqqY,1727
11
+ instructvault/spec.py,sha256=ZtVXosHy0f3hRB5CP9xbVzSdW8fDnf0-AR46ehG9-MA,2450
12
+ instructvault/store.py,sha256=NhN49w7xrkeij0lQDr-CEdANYLpNVBXumv_cKqLmiYY,1056
13
+ instructvault-0.2.3.dist-info/METADATA,sha256=1fSmJLHruAYXuZpJa6AbDVSDaG8SLp8_lrxlH3OHaEM,6205
14
+ instructvault-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
+ instructvault-0.2.3.dist-info/entry_points.txt,sha256=cdcMJQwBk9c95LwfN2W6x2xO43FwPjhfV3jHE7TTuHg,49
16
+ instructvault-0.2.3.dist-info/licenses/LICENSE,sha256=VFbCvIsyizmkz4NrZPMdcPhyRK5uM0HhAjv3GBUbb7Y,135
17
+ instructvault-0.2.3.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- instructvault/__init__.py,sha256=cg7j0qh6W84D-K0uSOLKKAP2JquW4NRXwZRDDLk5E18,59
2
- instructvault/bundle.py,sha256=gXOqjpza_8MrYJjJVCzBJKHbO78bXkAbqPUjKnS_6Nk,2478
3
- instructvault/cli.py,sha256=2ylpvw-GHmZdIUVS3AoepWcQy6RG9QAwqVEQaCFG60o,6065
4
- instructvault/diff.py,sha256=vz_vmKDXasNFoVKHCk2u_TsboHk1BdwvX0wCnJI1ATQ,252
5
- instructvault/eval.py,sha256=-yrFHCEUrONvzfKLP8s_RktFU74Ergp9tQJvzfrMR9s,1949
6
- instructvault/io.py,sha256=mxoNp6SXbFoty22fzrbf7z-B5Nlw0XjgWphK0awA1S8,867
7
- instructvault/junit.py,sha256=sIEcIiGD3Xk6uCYjnE5p_07j8dPoS_RAc2eoy3BIBeQ,1133
8
- instructvault/render.py,sha256=vcVnqIXGytskZEKbUofoKgIVflQSYhsmdpEtZs1X19A,919
9
- instructvault/scaffold.py,sha256=f5gwXE3dUPuJYTedZRqBs8w5SQEgt1dgDSuqW2dxrMg,1685
10
- instructvault/sdk.py,sha256=JpEik6AZof2cIG-4GO-mdWwZe7w2rwq45jKawwfstsQ,1594
11
- instructvault/spec.py,sha256=xRkcVjMnoKECLaAfjQHiGgQVMdZ_KvNzf-K-j6DQ6k0,1737
12
- instructvault/store.py,sha256=NhN49w7xrkeij0lQDr-CEdANYLpNVBXumv_cKqLmiYY,1056
13
- instructvault-0.2.1.dist-info/METADATA,sha256=rUEeRQAC23afX9UKccIqNKnGxCbKAmFaSQmnQxIaJew,5536
14
- instructvault-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
- instructvault-0.2.1.dist-info/entry_points.txt,sha256=cdcMJQwBk9c95LwfN2W6x2xO43FwPjhfV3jHE7TTuHg,49
16
- instructvault-0.2.1.dist-info/licenses/LICENSE,sha256=VFbCvIsyizmkz4NrZPMdcPhyRK5uM0HhAjv3GBUbb7Y,135
17
- instructvault-0.2.1.dist-info/RECORD,,