path-link 0.2.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.
@@ -0,0 +1,399 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ `ptool-serena` is a Python library for type-safe project path management with built-in validation. It uses Pydantic models to dynamically create path configurations from `pyproject.toml` or custom `.paths` files, and can generate static dataclass models for enhanced IDE support.
8
+
9
+ ## Development Setup
10
+
11
+ ### Environment
12
+
13
+ Uses `uv` for dependency management:
14
+
15
+ ```bash
16
+ # Setup environment
17
+ uv venv
18
+ source .venv/bin/activate # macOS/Linux
19
+ uv pip install -e ".[test]"
20
+ ```
21
+
22
+ The project must be installed in editable mode. Never modify `PYTHONPATH` - all imports resolve through the editable install.
23
+
24
+ ### Common Commands
25
+
26
+ ```bash
27
+ # Run all tests
28
+ uv run pytest
29
+
30
+ # Run tests with coverage
31
+ uv run pytest --cov=src --cov-report=term-missing
32
+
33
+ # Run specific test file
34
+ uv run pytest tests/test_validators.py
35
+
36
+ # Lint code
37
+ uv run ruff check .
38
+
39
+ # Format code
40
+ uv run ruff format .
41
+
42
+ # Type check
43
+ uv run mypy src/
44
+
45
+ # Regenerate static model (required after pyproject.toml changes)
46
+ uv run python -c "from project_paths import write_dataclass_file; write_dataclass_file()"
47
+
48
+ # Or use justfile commands
49
+ just test
50
+ just lint
51
+ just format
52
+ just regen
53
+ ```
54
+
55
+ ### Smoke Test
56
+
57
+ Verify setup is working:
58
+ ```bash
59
+ uv run python scripts/smoke_import.py
60
+ # Or one-liner:
61
+ uv run python -c "from project_paths import ProjectPaths; p = ProjectPaths.from_pyproject(); print('✅ OK:', len(p.to_dict()), 'paths loaded')"
62
+ ```
63
+
64
+ ## Architecture
65
+
66
+ ### Core Components
67
+
68
+ 1. **ProjectPaths Model** (`src/project_paths/model.py`)
69
+ - Main API for path management
70
+ - Uses Pydantic dynamic model creation via `create_model()`
71
+ - Direct instantiation is disabled - use factory methods only:
72
+ - `ProjectPaths.from_pyproject()` - loads from `[tool.project_paths]` in pyproject.toml
73
+ - `ProjectPaths.from_config(".paths")` - loads from dotenv-style files
74
+ - All path access returns `pathlib.Path` objects
75
+ - Supports dict-style access via `__getitem__`
76
+
77
+ 2. **Builder System** (`src/project_paths/builder.py`)
78
+ - `build_field_definitions()` - constructs Pydantic field definitions from config
79
+ - `make_path_factory()` - creates lazy path resolvers with environment variable expansion
80
+ - `get_paths_from_pyproject()` - TOML parser for pyproject.toml
81
+ - `get_paths_from_dot_paths()` - dotenv parser for .paths files
82
+
83
+ 3. **Validation Framework** (`src/project_paths/validation.py`)
84
+ - Protocol-based system using `PathValidator` protocol
85
+ - `ValidationResult` aggregates `Finding` objects with severity levels (INFO, WARNING, ERROR)
86
+ - `validate_or_raise()` helper throws `PathValidationError` on failure
87
+ - `CompositeValidator` chains multiple validators
88
+
89
+ 4. **Built-in Validators** (`src/project_paths/builtin_validators/`)
90
+ - `StrictPathValidator` - enforces path existence, type (file/dir), and symlink rules
91
+ - Configurable via `required`, `must_be_dir`, `must_be_file`, `allow_symlinks` parameters
92
+ - `SandboxPathValidator` - prevents path traversal attacks and enforces base directory sandbox
93
+ - Configurable via `base_dir_key`, `allow_absolute`, `strict_mode`, `check_paths` parameters
94
+ - Error codes: `PATH_TRAVERSAL_ATTEMPT`, `ABSOLUTE_PATH_BLOCKED`, `PATH_ESCAPES_SANDBOX`
95
+
96
+ 5. **Static Model Generation** (`src/project_paths/get_paths.py`)
97
+ - `write_dataclass_file()` generates `project_paths_static.py` from current config
98
+ - Uses atomic file replacement via temp file to avoid TOCTOU issues
99
+ - Must be regenerated after modifying `[tool.project_paths]` in pyproject.toml
100
+
101
+ 6. **Documentation Access** (`src/project_paths/__init__.py`)
102
+ - `get_ai_guidelines()` - Returns AI assistant usage patterns (bundled assistant_context.md)
103
+ - `get_developer_guide()` - Returns architecture and development guide (bundled CLAUDE.md)
104
+ - `get_metadata()` - Returns machine-readable project metadata (bundled assistant_handoff.json)
105
+ - Documentation bundled via `[tool.setuptools.package-data]` for offline access
106
+
107
+ ### Configuration Format
108
+
109
+ In `pyproject.toml`:
110
+ ```toml
111
+ [tool.project_paths.paths]
112
+ config_dir = "config"
113
+ data_dir = "data"
114
+
115
+ [tool.project_paths.files]
116
+ settings_file = "config/settings.json"
117
+ ```
118
+
119
+ In `.paths` files (dotenv format, no sections):
120
+ ```
121
+ config_dir=config
122
+ data_dir=data
123
+ settings_file=config/settings.json
124
+ ```
125
+
126
+ ### Import Structure
127
+
128
+ The project uses `src` layout. All imports should be absolute from package name:
129
+ ```python
130
+ from project_paths import ProjectPaths, ValidationResult, StrictPathValidator
131
+ from project_paths.model import _ProjectPathsBase
132
+ from project_paths.validation import Finding, Severity
133
+ ```
134
+
135
+ ## Critical Rules
136
+
137
+ ### ProjectPaths Usage
138
+
139
+ **Correct:**
140
+ ```python
141
+ from project_paths import ProjectPaths
142
+
143
+ # Load from pyproject.toml
144
+ paths = ProjectPaths.from_pyproject()
145
+
146
+ # Load from custom config
147
+ paths = ProjectPaths.from_config(".paths")
148
+
149
+ # Access paths
150
+ config = paths.config_dir
151
+ settings = paths["settings_file"]
152
+ ```
153
+
154
+ **Incorrect:**
155
+ ```python
156
+ # NEVER do this - direct instantiation is disabled
157
+ paths = ProjectPaths() # Raises NotImplementedError
158
+ ```
159
+
160
+ ### Static Model Sync
161
+
162
+ After any change to `[tool.project_paths]` in pyproject.toml, you MUST regenerate the static model:
163
+ ```bash
164
+ uv run python -c "from project_paths import write_dataclass_file; write_dataclass_file()"
165
+ ```
166
+
167
+ The CI check `just check-regen` verifies this file is in sync.
168
+
169
+ ### TOCTOU Prevention
170
+
171
+ Avoid time-of-check-time-of-use vulnerabilities in filesystem operations:
172
+
173
+ **Unsafe:**
174
+ ```python
175
+ if not path.exists():
176
+ path.mkdir() # Race condition possible
177
+ ```
178
+
179
+ **Safe:**
180
+ ```python
181
+ try:
182
+ path.mkdir(exist_ok=False)
183
+ except FileExistsError:
184
+ if not path.is_dir():
185
+ raise
186
+ ```
187
+
188
+ The `StrictPathValidator` includes `allow_symlinks` parameter to mitigate symlink-based attacks.
189
+
190
+ ### Path Traversal Protection
191
+
192
+ Use `SandboxPathValidator` to prevent path traversal attacks and ensure paths stay within the project sandbox:
193
+
194
+ **Maximum Security (Recommended):**
195
+ ```python
196
+ from project_paths import ProjectPaths, SandboxPathValidator
197
+
198
+ paths = ProjectPaths.from_pyproject()
199
+ validator = SandboxPathValidator(
200
+ base_dir_key="base_dir",
201
+ allow_absolute=False, # Block all absolute paths
202
+ strict_mode=True, # Block '..' patterns
203
+ check_paths=[] # Check all paths
204
+ )
205
+
206
+ result = validator.validate(paths)
207
+ if not result.ok():
208
+ for error in result.errors():
209
+ print(f"Security issue: {error.code} in {error.field}")
210
+ ```
211
+
212
+ **Permissive Mode (Allow absolute paths within sandbox):**
213
+ ```python
214
+ validator = SandboxPathValidator(
215
+ allow_absolute=True, # Allow absolute paths if within base_dir
216
+ strict_mode=False # Allow '..' if it resolves safely
217
+ )
218
+ ```
219
+
220
+ **Security Features:**
221
+ - Detects `..` path traversal patterns in strict mode
222
+ - Validates all paths resolve within `base_dir` after symlink resolution
223
+ - Configurable absolute path handling
224
+ - Clear error codes for security violations
225
+
226
+ **Error Codes:**
227
+ - `PATH_TRAVERSAL_ATTEMPT` - Path contains `..` pattern (strict mode only)
228
+ - `ABSOLUTE_PATH_BLOCKED` - Absolute path not allowed (when `allow_absolute=False`)
229
+ - `PATH_ESCAPES_SANDBOX` - Resolved path is outside `base_dir`
230
+ - `SANDBOX_BASE_MISSING` - Base directory key not found
231
+ - `SANDBOX_BASE_UNRESOLVABLE` - Cannot resolve base directory
232
+ - `PATH_UNRESOLVABLE` - Cannot resolve path (warning level)
233
+
234
+ ## Testing
235
+
236
+ ### Test Structure
237
+
238
+ - Tests mirror source structure: `src/project_paths/model.py` → `tests/test_model.py`
239
+ - Tests use pytest fixtures and `tmp_path` for isolated filesystem operations
240
+ - Mock `_ProjectPathsBase` instances in tests using `MagicMock(spec=_ProjectPathsBase)`
241
+
242
+ ### Running Tests
243
+
244
+ ```bash
245
+ # All tests
246
+ uv run pytest
247
+
248
+ # Specific test file
249
+ uv run pytest tests/test_validators.py
250
+
251
+ # With coverage
252
+ uv run pytest --cov=src --cov-report=term-missing
253
+
254
+ # Single test
255
+ uv run pytest tests/test_validators.py::test_strict_validator_required
256
+ ```
257
+
258
+ ## Validation Patterns
259
+
260
+ ### Basic Validation
261
+ ```python
262
+ from project_paths import ProjectPaths, validate_or_raise, StrictPathValidator
263
+
264
+ paths = ProjectPaths.from_pyproject()
265
+ validator = StrictPathValidator(
266
+ required=["config_dir", "data_dir"],
267
+ must_be_dir=["config_dir"],
268
+ must_be_file=["settings_file"]
269
+ )
270
+
271
+ # Raises PathValidationError on failure
272
+ validate_or_raise(paths, validator)
273
+ ```
274
+
275
+ ### Composite Validation
276
+ ```python
277
+ from project_paths import CompositeValidator
278
+
279
+ validator = CompositeValidator(parts=[
280
+ StrictPathValidator(required=["config_dir"]),
281
+ CustomValidator()
282
+ ])
283
+
284
+ result = validator.validate(paths)
285
+ if not result.ok():
286
+ for error in result.errors():
287
+ print(f"{error.code}: {error.message}")
288
+ ```
289
+
290
+ ### Custom Validators
291
+
292
+ Implement the `PathValidator` protocol:
293
+ ```python
294
+ from dataclasses import dataclass
295
+ from project_paths import ValidationResult, Finding, Severity
296
+
297
+ @dataclass
298
+ class CustomValidator:
299
+ def validate(self, paths) -> ValidationResult:
300
+ result = ValidationResult()
301
+ # Add validation logic
302
+ if some_condition:
303
+ result.add(Finding(
304
+ severity=Severity.ERROR,
305
+ code="CUSTOM_ERROR",
306
+ field="field_name",
307
+ message="Error description"
308
+ ))
309
+ return result
310
+ ```
311
+
312
+ ## Accessing Documentation Programmatically
313
+
314
+ The package includes bundled documentation accessible via three helper functions. This enables offline access and is especially useful for AI assistants helping users.
315
+
316
+ ```python
317
+ from project_paths import get_ai_guidelines, get_developer_guide, get_metadata
318
+ import json
319
+
320
+ # Access AI assistant guidelines (assistant_context.md)
321
+ ai_docs = get_ai_guidelines()
322
+
323
+ # Access developer guide (this file - CLAUDE.md)
324
+ dev_docs = get_developer_guide()
325
+
326
+ # Access machine-readable metadata (assistant_handoff.json)
327
+ metadata = json.loads(get_metadata())
328
+ print(f"Version: {metadata['version']}")
329
+ print(f"Public APIs: {len(metadata['public_api'])}")
330
+ ```
331
+
332
+ **Location:** Documentation files are stored in `src/project_paths/docs/` and bundled with the package via `[tool.setuptools.package-data]` in `pyproject.toml`.
333
+
334
+ **Use Cases:**
335
+ - AI assistants helping users with the package
336
+ - Offline/airgapped environments
337
+ - Programmatic access to package metadata
338
+
339
+ ## Package Structure
340
+
341
+ ```
342
+ src/project_paths/
343
+ ├── __init__.py # Public API exports + documentation access functions
344
+ ├── model.py # ProjectPaths class and factory methods
345
+ ├── builder.py # Config loading and field building
346
+ ├── get_paths.py # Static model generation
347
+ ├── validation.py # Validation framework
348
+ ├── project_paths_static.py # Auto-generated static model
349
+ ├── cli.py # CLI tool (ptool command)
350
+ ├── builtin_validators/
351
+ │ ├── __init__.py
352
+ │ ├── strict.py # StrictPathValidator
353
+ │ └── sandbox.py # SandboxPathValidator
354
+ └── docs/ # Bundled documentation (accessible offline)
355
+ ├── ai_guidelines.md # AI assistant usage patterns (assistant_context.md)
356
+ ├── developer_guide.md # Architecture & development (CLAUDE.md)
357
+ └── metadata.json # Machine-readable metadata (assistant_handoff.json)
358
+
359
+ tests/
360
+ ├── test_validators.py
361
+ ├── test_sandbox_validator.py
362
+ ├── test_static_model_equivalence.py
363
+ ├── test_path_policy.py
364
+ ├── test_env_expansion.py
365
+ ├── test_cli.py
366
+ └── test_example_project.py
367
+
368
+ scripts/
369
+ ├── smoke_import.py # Quick verification script
370
+ └── test_coverage_tool.py # Test coverage analysis
371
+ ```
372
+
373
+ ## Dependencies
374
+
375
+ **Runtime:**
376
+ - `pydantic>=2.11.0` - Dynamic model creation and validation
377
+ - `python-dotenv>=1.0.1` - Parsing .paths files
378
+
379
+ **Development:**
380
+ - `pytest` - Test framework
381
+ - `pytest-cov` - Coverage reporting
382
+ - `mypy` - Type checking
383
+ - `ruff` - Linting and formatting
384
+
385
+ ## Troubleshooting
386
+
387
+ ### Import errors
388
+ - Ensure virtual environment is activated
389
+ - Verify editable install: `uv pip install -e ".[test]"`
390
+ - Check import uses package name: `from project_paths import ...`
391
+
392
+ ### Static model out of sync
393
+ - Run: `just check-regen` to detect
394
+ - Fix with: `just regen`
395
+
396
+ ### Test failures
397
+ - Use `tmp_path` fixture for filesystem tests
398
+ - Mock `_ProjectPathsBase` when testing validators
399
+ - Ensure tests are isolated and don't depend on cwd state
@@ -0,0 +1,119 @@
1
+ {
2
+ "dist_name": "ptool-serena",
3
+ "import_name": "project_paths",
4
+ "version": "0.2.0",
5
+ "factories": [
6
+ "ProjectPaths.from_pyproject",
7
+ "ProjectPaths.from_config"
8
+ ],
9
+ "cli_commands": [
10
+ {
11
+ "command": "ptool print",
12
+ "description": "Print all configured paths as JSON",
13
+ "options": ["--source {pyproject,config}", "--config PATH"]
14
+ },
15
+ {
16
+ "command": "ptool validate",
17
+ "description": "Validate project structure",
18
+ "options": ["--source {pyproject,config}", "--config PATH", "--strict", "--raise"]
19
+ },
20
+ {
21
+ "command": "ptool gen-static",
22
+ "description": "Generate static dataclass model",
23
+ "options": ["--out PATH"]
24
+ }
25
+ ],
26
+ "validators": [
27
+ {
28
+ "name": "StrictPathValidator",
29
+ "module": "project_paths.builtin_validators.strict",
30
+ "description": "Ensures paths exist and match expected types (file/directory)"
31
+ },
32
+ {
33
+ "name": "SandboxPathValidator",
34
+ "module": "project_paths.builtin_validators.sandbox",
35
+ "description": "Prevents path traversal attacks and enforces base directory sandbox"
36
+ },
37
+ {
38
+ "name": "CompositeValidator",
39
+ "module": "project_paths.validation",
40
+ "description": "Combines multiple validators into a single validation pipeline"
41
+ }
42
+ ],
43
+ "public_api": [
44
+ "ProjectPaths",
45
+ "write_dataclass_file",
46
+ "Severity",
47
+ "Finding",
48
+ "ValidationResult",
49
+ "PathValidator",
50
+ "PathValidationError",
51
+ "validate_or_raise",
52
+ "CompositeValidator",
53
+ "StrictPathValidator",
54
+ "SandboxPathValidator",
55
+ "get_ai_guidelines",
56
+ "get_developer_guide",
57
+ "get_metadata"
58
+ ],
59
+ "invariants": [
60
+ "No direct constructor calls (raises NotImplementedError)",
61
+ "Key=value .paths (no sections, dotenv format)",
62
+ "All paths are pathlib.Path objects",
63
+ "Static model must be regenerated after pyproject.toml changes"
64
+ ],
65
+ "test_status": {
66
+ "passing": 53,
67
+ "coverage": "60%",
68
+ "target_coverage": "90%",
69
+ "last_verified": "2025-10-10"
70
+ },
71
+ "critical_commands": {
72
+ "smoke_test": "uv run python -c \"from project_paths import ProjectPaths; p = ProjectPaths.from_pyproject(); print('✅ OK:', len(p.to_dict()), 'paths loaded')\"",
73
+ "run_tests": "uv run pytest",
74
+ "check_coverage": "uv run pytest --cov=src --cov-report=term-missing:skip-covered",
75
+ "regenerate_static": "uv run python -c \"from project_paths import write_dataclass_file; write_dataclass_file()\"",
76
+ "verify_static_sync": "just check-regen"
77
+ },
78
+ "known_issues": {
79
+ "resolved": [
80
+ "main.py direct constructor call - FIXED 2025-10-10",
81
+ "Static model base_dir hardcoding - FIXED 2025-10-10",
82
+ "SandboxPathValidator not exported - FIXED 2025-10-10"
83
+ ],
84
+ "active": []
85
+ },
86
+ "architecture_notes": {
87
+ "pattern": "Dynamic Pydantic model creation via create_model()",
88
+ "validation": "Protocol-based with ValidationResult/Finding pattern",
89
+ "key_files": [
90
+ "src/project_paths/model.py - Factory methods and _ProjectPathsBase",
91
+ "src/project_paths/builder.py - Config loading and field definitions",
92
+ "src/project_paths/validation.py - Validation framework",
93
+ "src/project_paths/builtin_validators/strict.py - StrictPathValidator",
94
+ "src/project_paths/builtin_validators/sandbox.py - SandboxPathValidator",
95
+ "src/project_paths/get_paths.py - Static model generation"
96
+ ]
97
+ },
98
+ "policy_compliance": {
99
+ "code_quality_standard": "CODE_QUALITY.json (SOLID, KISS, YAGNI)",
100
+ "reasoning_framework": "CHAIN_OF_THOUGHT_GOLDEN_ALL_IN_ONE.json",
101
+ "violations": 0,
102
+ "last_audit": "2025-10-10"
103
+ },
104
+ "documentation": {
105
+ "user_guide": "README.md",
106
+ "dev_guide": "CLAUDE.md",
107
+ "refactor_plan": "REFACTOR_PLAN_1.md",
108
+ "refactor_status": "REFACTOR_STATUS.md",
109
+ "context": "assistant_context.md",
110
+ "bundled_in_package": true,
111
+ "access_functions": {
112
+ "get_ai_guidelines": "Returns assistant_context.md content (AI usage patterns)",
113
+ "get_developer_guide": "Returns CLAUDE.md content (architecture & dev setup)",
114
+ "get_metadata": "Returns this file as JSON (machine-readable metadata)"
115
+ },
116
+ "package_location": "src/project_paths/docs/",
117
+ "offline_accessible": true
118
+ }
119
+ }
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Optional, Union
5
+
6
+ from .model import ProjectPaths
7
+
8
+
9
+ def generate_static_model_text() -> str:
10
+ """
11
+ Generates the Python code for the static dataclass model.
12
+ """
13
+ lines = [
14
+ "from pathlib import Path",
15
+ "from dataclasses import dataclass, field",
16
+ "",
17
+ "# This file is auto-generated by ptool-serena. Do not edit manually.",
18
+ "# Run `ptool gen-static` or `just regen` to regenerate.",
19
+ "",
20
+ "@dataclass(frozen=True)",
21
+ "class ProjectPathsStatic:",
22
+ ' """A static, auto-generated dataclass for project paths."""',
23
+ "",
24
+ ]
25
+
26
+ # Instantiate from pyproject.toml to get the default fields
27
+ model = ProjectPaths.from_pyproject()
28
+
29
+ # Use Pydantic's model_fields to get the defined fields and their defaults
30
+ for field_name, field_info in model.model_fields.items():
31
+ # The default_factory is a lambda, we need to call it to get the path
32
+ if field_info.default_factory:
33
+ default_path = field_info.default_factory() # type: ignore[call-arg,misc]
34
+
35
+ # Special case: base_dir should use Path.cwd() for portability
36
+ if field_name == "base_dir":
37
+ line = f" {field_name}: Path = field(default_factory=Path.cwd)"
38
+ # Special case for Path.home() to use the factory
39
+ elif default_path == Path.home():
40
+ line = f" {field_name}: Path = field(default_factory=Path.home)"
41
+ else:
42
+ # We want to store the path as a string relative to the base_dir for portability
43
+ # Determine if the path is truly inside the base directory.
44
+ # Using os.path.relpath can be tricky across OSes and for non-subpaths.
45
+ # A more reliable check is to see if base_dir is one of the parents of the path.
46
+ base_dir = getattr(model, "base_dir", Path.cwd())
47
+ is_subpath = base_dir.resolve() in default_path.resolve().parents
48
+
49
+ if is_subpath:
50
+ # For paths inside the project, make them relative for portability.
51
+ rel_path = os.path.relpath(
52
+ default_path.resolve(), base_dir.resolve()
53
+ )
54
+ rel_path_posix = str(Path(rel_path).as_posix())
55
+ line = f' {field_name}: Path = field(default_factory=lambda: Path.cwd() / "{rel_path_posix}")'
56
+ else:
57
+ # For external absolute paths, store them directly.
58
+ line = f' {field_name}: Path = field(default=Path("{default_path}"))'
59
+
60
+ else:
61
+ # Handle fields with a simple default value
62
+ line = (
63
+ f' {field_name}: Path = field(default=Path("{field_info.default}"))'
64
+ )
65
+
66
+ lines.append(line)
67
+
68
+ return "\n".join(lines)
69
+
70
+
71
+ def write_dataclass_file(
72
+ output_path: Optional[Union[str, Path]] = None,
73
+ class_name: str = "ProjectPathsStatic", # class_name is not used yet, but kept for future
74
+ ) -> None:
75
+ """
76
+ Generates and writes the static dataclass file.
77
+ """
78
+ if output_path is None:
79
+ # Default path within the src directory
80
+ output_path = Path(__file__).parent / "project_paths_static.py"
81
+
82
+ resolved_path = Path(output_path).resolve()
83
+ resolved_path.parent.mkdir(parents=True, exist_ok=True)
84
+
85
+ # Generate the code
86
+ generated_code = generate_static_model_text()
87
+
88
+ # Write to a temporary file first to avoid race conditions (TOCTOU)
89
+ temp_path = resolved_path.with_suffix(".tmp")
90
+
91
+ # Write and sync to disk before replace (prevents TOCTOU vulnerability)
92
+ with temp_path.open("w", encoding="utf-8") as f:
93
+ f.write(generated_code)
94
+ f.flush()
95
+ os.fsync(f.fileno()) # Ensure data is written to disk
96
+
97
+ # Atomically replace the old file with the new one (atomic on POSIX)
98
+ temp_path.replace(resolved_path)
99
+
100
+ # Verify the file exists (after atomic replace, no TOCTOU risk)
101
+ if resolved_path.exists():
102
+ print(f"✅ Static model written to {resolved_path}")
103
+ else:
104
+ raise FileNotFoundError(f"❌ Failed to write static model to {resolved_path}")
project_paths/main.py ADDED
@@ -0,0 +1,2 @@
1
+ def main():
2
+ print("🔧 Project paths CLI tool – coming soon!")
project_paths/model.py ADDED
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ from typing import Union, Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, create_model
6
+
7
+ from .builder import (
8
+ build_field_definitions,
9
+ get_paths_from_dot_paths,
10
+ get_paths_from_pyproject,
11
+ )
12
+
13
+
14
+ class _ProjectPathsBase(BaseModel):
15
+ """
16
+ Internal base class for ProjectPaths.
17
+ This class has a functional __init__ and provides the core logic.
18
+ It should not be used directly by end-users.
19
+ """
20
+
21
+ model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True)
22
+
23
+ def to_dict(self) -> dict[str, Path]:
24
+ """Returns a dictionary of all resolved path attributes."""
25
+ return self.model_dump(include=set(self.model_fields.keys())) # type: ignore[no-any-return]
26
+
27
+ def get_paths(self) -> list[Path]:
28
+ """Returns a list of all resolved Path objects."""
29
+ return [v for v in self.to_dict().values() if isinstance(v, Path)]
30
+
31
+ def __getitem__(self, key: str) -> Path:
32
+ """Enables dictionary-style access to path attributes."""
33
+ if key not in self.model_fields:
34
+ raise KeyError(f"'{key}' is not a configured path.")
35
+ return getattr(self, key) # type: ignore[no-any-return]
36
+
37
+
38
+ class ProjectPaths:
39
+ """
40
+ Main path management class.
41
+
42
+ Do not instantiate this class directly. Use the factory methods:
43
+ - `ProjectPaths.from_pyproject()`
44
+ - `ProjectPaths.from_config("path/to/.paths")`
45
+ """
46
+
47
+ def __init__(self, **kwargs: Any):
48
+ raise NotImplementedError(
49
+ "Direct instantiation of ProjectPaths is not supported. "
50
+ "Use a factory method: `from_pyproject()` or `from_config()`."
51
+ )
52
+
53
+ @classmethod
54
+ def from_pyproject(cls) -> _ProjectPathsBase:
55
+ """Creates a ProjectPaths instance from pyproject.toml."""
56
+ field_defs = build_field_definitions(loader_func=get_paths_from_pyproject)
57
+ DynamicModel = create_model( # type: ignore[call-overload]
58
+ "ProjectPathsDynamic",
59
+ __base__=(_ProjectPathsBase,), # Config inherited from base class
60
+ **field_defs,
61
+ )
62
+ return DynamicModel() # type: ignore[no-any-return]
63
+
64
+ @classmethod
65
+ def from_config(cls, config_path: Union[str, Path]) -> _ProjectPathsBase:
66
+ """Creates a ProjectPaths instance from a custom .paths file."""
67
+ resolved_path = Path(config_path)
68
+ field_defs = build_field_definitions(
69
+ loader_func=get_paths_from_dot_paths, config_path=resolved_path
70
+ )
71
+ DynamicModel = create_model( # type: ignore[call-overload]
72
+ "ProjectPathsDynamic",
73
+ __base__=(_ProjectPathsBase,), # Config inherited from base class
74
+ **field_defs,
75
+ )
76
+ return DynamicModel() # type: ignore[no-any-return]