path-link 0.2.0__py3-none-any.whl → 0.3.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.
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
4
 
5
5
  ## Project Overview
6
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.
7
+ `path-link` 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
8
 
9
9
  ## Development Setup
10
10
 
@@ -43,7 +43,7 @@ uv run ruff format .
43
43
  uv run mypy src/
44
44
 
45
45
  # Regenerate static model (required after pyproject.toml changes)
46
- uv run python -c "from project_paths import write_dataclass_file; write_dataclass_file()"
46
+ uv run python -c "from path_link import write_dataclass_file; write_dataclass_file()"
47
47
 
48
48
  # Or use justfile commands
49
49
  just test
@@ -58,7 +58,7 @@ Verify setup is working:
58
58
  ```bash
59
59
  uv run python scripts/smoke_import.py
60
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')"
61
+ uv run python -c "from path_link import ProjectPaths; p = ProjectPaths.from_pyproject(); print('✅ OK:', len(p.to_dict()), 'paths loaded')"
62
62
  ```
63
63
 
64
64
  ## Architecture
@@ -69,7 +69,7 @@ uv run python -c "from project_paths import ProjectPaths; p = ProjectPaths.from_
69
69
  - Main API for path management
70
70
  - Uses Pydantic dynamic model creation via `create_model()`
71
71
  - Direct instantiation is disabled - use factory methods only:
72
- - `ProjectPaths.from_pyproject()` - loads from `[tool.project_paths]` in pyproject.toml
72
+ - `ProjectPaths.from_pyproject()` - loads from `[tool.path_link]` in pyproject.toml
73
73
  - `ProjectPaths.from_config(".paths")` - loads from dotenv-style files
74
74
  - All path access returns `pathlib.Path` objects
75
75
  - Supports dict-style access via `__getitem__`
@@ -96,7 +96,7 @@ uv run python -c "from project_paths import ProjectPaths; p = ProjectPaths.from_
96
96
  5. **Static Model Generation** (`src/project_paths/get_paths.py`)
97
97
  - `write_dataclass_file()` generates `project_paths_static.py` from current config
98
98
  - Uses atomic file replacement via temp file to avoid TOCTOU issues
99
- - Must be regenerated after modifying `[tool.project_paths]` in pyproject.toml
99
+ - Must be regenerated after modifying `[tool.path_link]` in pyproject.toml
100
100
 
101
101
  6. **Documentation Access** (`src/project_paths/__init__.py`)
102
102
  - `get_ai_guidelines()` - Returns AI assistant usage patterns (bundled assistant_context.md)
@@ -108,11 +108,11 @@ uv run python -c "from project_paths import ProjectPaths; p = ProjectPaths.from_
108
108
 
109
109
  In `pyproject.toml`:
110
110
  ```toml
111
- [tool.project_paths.paths]
111
+ [tool.path_link.paths]
112
112
  config_dir = "config"
113
113
  data_dir = "data"
114
114
 
115
- [tool.project_paths.files]
115
+ [tool.path_link.files]
116
116
  settings_file = "config/settings.json"
117
117
  ```
118
118
 
@@ -127,9 +127,9 @@ settings_file=config/settings.json
127
127
 
128
128
  The project uses `src` layout. All imports should be absolute from package name:
129
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
130
+ from path_link import ProjectPaths, ValidationResult, StrictPathValidator
131
+ from path_link.model import _ProjectPathsBase
132
+ from path_link.validation import Finding, Severity
133
133
  ```
134
134
 
135
135
  ## Critical Rules
@@ -138,7 +138,7 @@ from project_paths.validation import Finding, Severity
138
138
 
139
139
  **Correct:**
140
140
  ```python
141
- from project_paths import ProjectPaths
141
+ from path_link import ProjectPaths
142
142
 
143
143
  # Load from pyproject.toml
144
144
  paths = ProjectPaths.from_pyproject()
@@ -159,9 +159,9 @@ paths = ProjectPaths() # Raises NotImplementedError
159
159
 
160
160
  ### Static Model Sync
161
161
 
162
- After any change to `[tool.project_paths]` in pyproject.toml, you MUST regenerate the static model:
162
+ After any change to `[tool.path_link]` in pyproject.toml, you MUST regenerate the static model:
163
163
  ```bash
164
- uv run python -c "from project_paths import write_dataclass_file; write_dataclass_file()"
164
+ uv run python -c "from path_link import write_dataclass_file; write_dataclass_file()"
165
165
  ```
166
166
 
167
167
  The CI check `just check-regen` verifies this file is in sync.
@@ -193,7 +193,7 @@ Use `SandboxPathValidator` to prevent path traversal attacks and ensure paths st
193
193
 
194
194
  **Maximum Security (Recommended):**
195
195
  ```python
196
- from project_paths import ProjectPaths, SandboxPathValidator
196
+ from path_link import ProjectPaths, SandboxPathValidator
197
197
 
198
198
  paths = ProjectPaths.from_pyproject()
199
199
  validator = SandboxPathValidator(
@@ -259,7 +259,7 @@ uv run pytest tests/test_validators.py::test_strict_validator_required
259
259
 
260
260
  ### Basic Validation
261
261
  ```python
262
- from project_paths import ProjectPaths, validate_or_raise, StrictPathValidator
262
+ from path_link import ProjectPaths, validate_or_raise, StrictPathValidator
263
263
 
264
264
  paths = ProjectPaths.from_pyproject()
265
265
  validator = StrictPathValidator(
@@ -274,7 +274,7 @@ validate_or_raise(paths, validator)
274
274
 
275
275
  ### Composite Validation
276
276
  ```python
277
- from project_paths import CompositeValidator
277
+ from path_link import CompositeValidator
278
278
 
279
279
  validator = CompositeValidator(parts=[
280
280
  StrictPathValidator(required=["config_dir"]),
@@ -292,7 +292,7 @@ if not result.ok():
292
292
  Implement the `PathValidator` protocol:
293
293
  ```python
294
294
  from dataclasses import dataclass
295
- from project_paths import ValidationResult, Finding, Severity
295
+ from path_link import ValidationResult, Finding, Severity
296
296
 
297
297
  @dataclass
298
298
  class CustomValidator:
@@ -314,7 +314,7 @@ class CustomValidator:
314
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
315
 
316
316
  ```python
317
- from project_paths import get_ai_guidelines, get_developer_guide, get_metadata
317
+ from path_link import get_ai_guidelines, get_developer_guide, get_metadata
318
318
  import json
319
319
 
320
320
  # Access AI assistant guidelines (assistant_context.md)
@@ -387,7 +387,7 @@ scripts/
387
387
  ### Import errors
388
388
  - Ensure virtual environment is activated
389
389
  - Verify editable install: `uv pip install -e ".[test]"`
390
- - Check import uses package name: `from project_paths import ...`
390
+ - Check import uses package name: `from path_link import ...`
391
391
 
392
392
  ### Static model out of sync
393
393
  - Run: `just check-regen` to detect
@@ -1,5 +1,5 @@
1
1
  {
2
- "dist_name": "ptool-serena",
2
+ "dist_name": "path-link",
3
3
  "import_name": "project_paths",
4
4
  "version": "0.2.0",
5
5
  "factories": [
@@ -26,17 +26,17 @@
26
26
  "validators": [
27
27
  {
28
28
  "name": "StrictPathValidator",
29
- "module": "project_paths.builtin_validators.strict",
29
+ "module": "path_link.builtin_validators.strict",
30
30
  "description": "Ensures paths exist and match expected types (file/directory)"
31
31
  },
32
32
  {
33
33
  "name": "SandboxPathValidator",
34
- "module": "project_paths.builtin_validators.sandbox",
34
+ "module": "path_link.builtin_validators.sandbox",
35
35
  "description": "Prevents path traversal attacks and enforces base directory sandbox"
36
36
  },
37
37
  {
38
38
  "name": "CompositeValidator",
39
- "module": "project_paths.validation",
39
+ "module": "path_link.validation",
40
40
  "description": "Combines multiple validators into a single validation pipeline"
41
41
  }
42
42
  ],
@@ -69,10 +69,10 @@
69
69
  "last_verified": "2025-10-10"
70
70
  },
71
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')\"",
72
+ "smoke_test": "uv run python -c \"from path_link import ProjectPaths; p = ProjectPaths.from_pyproject(); print('✅ OK:', len(p.to_dict()), 'paths loaded')\"",
73
73
  "run_tests": "uv run pytest",
74
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()\"",
75
+ "regenerate_static": "uv run python -c \"from path_link import write_dataclass_file; write_dataclass_file()\"",
76
76
  "verify_static_sync": "just check-regen"
77
77
  },
78
78
  "known_issues": {
@@ -3,7 +3,7 @@ import os
3
3
  from pathlib import Path
4
4
  from typing import Optional, Union
5
5
 
6
- from .model import ProjectPaths
6
+ from path_link.model import ProjectPaths
7
7
 
8
8
 
9
9
  def generate_static_model_text() -> str:
@@ -14,8 +14,8 @@ def generate_static_model_text() -> str:
14
14
  "from pathlib import Path",
15
15
  "from dataclasses import dataclass, field",
16
16
  "",
17
- "# This file is auto-generated by ptool-serena. Do not edit manually.",
18
- "# Run `ptool gen-static` or `just regen` to regenerate.",
17
+ "# This file is auto-generated by path-link. Do not edit manually.",
18
+ "# Run `pathlink gen-static` or `just regen` to regenerate.",
19
19
  "",
20
20
  "@dataclass(frozen=True)",
21
21
  "class ProjectPathsStatic:",
@@ -4,7 +4,7 @@ from typing import Union, Any
4
4
 
5
5
  from pydantic import BaseModel, ConfigDict, create_model
6
6
 
7
- from .builder import (
7
+ from path_link.builder import (
8
8
  build_field_definitions,
9
9
  get_paths_from_dot_paths,
10
10
  get_paths_from_pyproject,
@@ -1,8 +1,8 @@
1
1
  from pathlib import Path
2
2
  from dataclasses import dataclass, field
3
3
 
4
- # This file is auto-generated by ptool-serena. Do not edit manually.
5
- # Run `ptool gen-static` or `just regen` to regenerate.
4
+ # This file is auto-generated by path-link. Do not edit manually.
5
+ # Run `pathlink gen-static` or `just regen` to regenerate.
6
6
 
7
7
  @dataclass(frozen=True)
8
8
  class ProjectPathsStatic:
@@ -0,0 +1,225 @@
1
+ """Factory for creating ProjectUrls instances with validation mode support.
2
+
3
+ This module provides factory methods to create ProjectUrls instances from
4
+ various configuration sources (pyproject.toml, .urls files) with configurable
5
+ validation modes (lenient or strict).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+ from typing import Union, Any
13
+
14
+ from pydantic import create_model, Field
15
+
16
+ from path_link.url_model import _ProjectUrlsBase, ValidationMode, validate_url
17
+ from path_link.builder import (
18
+ get_urls_from_pyproject,
19
+ get_urls_from_dot_urls,
20
+ get_urls_merged,
21
+ )
22
+
23
+
24
+ def _get_validation_mode_from_env() -> ValidationMode:
25
+ """
26
+ Determine validation mode from environment variable.
27
+
28
+ Checks PTOOL_URL_MODE environment variable:
29
+ - "strict" -> ValidationMode.STRICT
30
+ - anything else -> ValidationMode.LENIENT (default)
31
+
32
+ Returns:
33
+ ValidationMode enum value
34
+ """
35
+ mode_str = os.getenv("PTOOL_URL_MODE", "lenient").lower()
36
+ if mode_str == "strict":
37
+ return ValidationMode.STRICT
38
+ return ValidationMode.LENIENT
39
+
40
+
41
+ def _make_url_field(url_value: str, mode: ValidationMode) -> tuple[type[str], Any]:
42
+ """
43
+ Create a Pydantic field definition for a URL with validation.
44
+
45
+ Args:
46
+ url_value: The URL string to validate
47
+ mode: Validation mode (lenient or strict)
48
+
49
+ Returns:
50
+ Tuple of (type, Field) for Pydantic model definition
51
+ """
52
+ # Validate the URL according to the mode
53
+ validated_url = validate_url(url_value, mode)
54
+
55
+ # Return a field with the validated URL as default
56
+ return (str, Field(default=validated_url))
57
+
58
+
59
+ def _build_url_field_definitions(
60
+ url_dict: dict[str, str], mode: ValidationMode
61
+ ) -> dict[str, tuple[type[str], Any]]:
62
+ """
63
+ Build Pydantic field definitions from URL dictionary.
64
+
65
+ Args:
66
+ url_dict: Dictionary of URL key-value pairs
67
+ mode: Validation mode to use for all URLs
68
+
69
+ Returns:
70
+ Dictionary of field definitions for dynamic Pydantic model
71
+ """
72
+ fields = {}
73
+
74
+ for key, url_value in url_dict.items():
75
+ fields[key] = _make_url_field(url_value, mode)
76
+
77
+ return fields
78
+
79
+
80
+ class ProjectUrls:
81
+ """
82
+ Main URL management class.
83
+
84
+ Do not instantiate this class directly. Use the factory methods:
85
+ - `ProjectUrls.from_pyproject(mode=ValidationMode.LENIENT)`
86
+ - `ProjectUrls.from_config("path/to/.urls", mode=ValidationMode.LENIENT)`
87
+ - `ProjectUrls.from_merged(mode=ValidationMode.LENIENT)`
88
+ """
89
+
90
+ def __init__(self, **kwargs: Any):
91
+ raise NotImplementedError(
92
+ "Direct instantiation of ProjectUrls is not supported. "
93
+ "Use a factory method: `from_pyproject()`, `from_config()`, or `from_merged()`."
94
+ )
95
+
96
+ @classmethod
97
+ def from_pyproject(
98
+ cls, mode: Union[ValidationMode, str, None] = None
99
+ ) -> _ProjectUrlsBase:
100
+ """
101
+ Creates a ProjectUrls instance from pyproject.toml [tool.path_link.urls].
102
+
103
+ Args:
104
+ mode: Validation mode (lenient or strict). If None, reads from
105
+ PTOOL_URL_MODE environment variable (default: lenient)
106
+
107
+ Returns:
108
+ Dynamic Pydantic model instance with validated URLs
109
+
110
+ Raises:
111
+ ValidationError: If any URL fails validation for the given mode
112
+ TypeError: If urls section is not a dict in pyproject.toml
113
+ """
114
+ # Determine validation mode
115
+ if mode is None:
116
+ validation_mode = _get_validation_mode_from_env()
117
+ elif isinstance(mode, str):
118
+ validation_mode = ValidationMode(mode.lower())
119
+ else:
120
+ validation_mode = mode
121
+
122
+ # Load URLs from pyproject.toml
123
+ url_dict = get_urls_from_pyproject()
124
+
125
+ # Build field definitions with validation
126
+ field_defs = _build_url_field_definitions(url_dict, validation_mode)
127
+
128
+ # Create dynamic model
129
+ DynamicModel = create_model( # type: ignore[call-overload]
130
+ "ProjectUrlsDynamic",
131
+ __base__=(_ProjectUrlsBase,),
132
+ **field_defs,
133
+ )
134
+
135
+ return DynamicModel() # type: ignore[no-any-return]
136
+
137
+ @classmethod
138
+ def from_config(
139
+ cls, config_path: Union[str, Path], mode: Union[ValidationMode, str, None] = None
140
+ ) -> _ProjectUrlsBase:
141
+ """
142
+ Creates a ProjectUrls instance from a .urls file.
143
+
144
+ Args:
145
+ config_path: Path to .urls configuration file
146
+ mode: Validation mode (lenient or strict). If None, reads from
147
+ PTOOL_URL_MODE environment variable (default: lenient)
148
+
149
+ Returns:
150
+ Dynamic Pydantic model instance with validated URLs
151
+
152
+ Raises:
153
+ FileNotFoundError: If config file doesn't exist
154
+ ValidationError: If any URL fails validation for the given mode
155
+ """
156
+ # Determine validation mode
157
+ if mode is None:
158
+ validation_mode = _get_validation_mode_from_env()
159
+ elif isinstance(mode, str):
160
+ validation_mode = ValidationMode(mode.lower())
161
+ else:
162
+ validation_mode = mode
163
+
164
+ # Load URLs from .urls file
165
+ resolved_path = Path(config_path)
166
+ url_dict = get_urls_from_dot_urls(resolved_path)
167
+
168
+ # Build field definitions with validation
169
+ field_defs = _build_url_field_definitions(url_dict, validation_mode)
170
+
171
+ # Create dynamic model
172
+ DynamicModel = create_model( # type: ignore[call-overload]
173
+ "ProjectUrlsDynamic",
174
+ __base__=(_ProjectUrlsBase,),
175
+ **field_defs,
176
+ )
177
+
178
+ return DynamicModel() # type: ignore[no-any-return]
179
+
180
+ @classmethod
181
+ def from_merged(
182
+ cls,
183
+ dotenv_path: Union[str, Path, None] = None,
184
+ mode: Union[ValidationMode, str, None] = None,
185
+ ) -> _ProjectUrlsBase:
186
+ """
187
+ Creates a ProjectUrls instance from merged pyproject.toml and .urls sources.
188
+
189
+ Precedence: pyproject.toml > .urls file
190
+ Missing keys are merged; duplicates resolved by pyproject.toml.
191
+
192
+ Args:
193
+ dotenv_path: Optional path to .urls file (default: ./.urls)
194
+ mode: Validation mode (lenient or strict). If None, reads from
195
+ PTOOL_URL_MODE environment variable (default: lenient)
196
+
197
+ Returns:
198
+ Dynamic Pydantic model instance with validated URLs
199
+
200
+ Raises:
201
+ ValidationError: If any URL fails validation for the given mode
202
+ """
203
+ # Determine validation mode
204
+ if mode is None:
205
+ validation_mode = _get_validation_mode_from_env()
206
+ elif isinstance(mode, str):
207
+ validation_mode = ValidationMode(mode.lower())
208
+ else:
209
+ validation_mode = mode
210
+
211
+ # Load merged URLs
212
+ resolved_dotenv = Path(dotenv_path) if dotenv_path else None
213
+ url_dict = get_urls_merged(resolved_dotenv)
214
+
215
+ # Build field definitions with validation
216
+ field_defs = _build_url_field_definitions(url_dict, validation_mode)
217
+
218
+ # Create dynamic model
219
+ DynamicModel = create_model( # type: ignore[call-overload]
220
+ "ProjectUrlsDynamic",
221
+ __base__=(_ProjectUrlsBase,),
222
+ **field_defs,
223
+ )
224
+
225
+ return DynamicModel() # type: ignore[no-any-return]
path_link/url_model.py ADDED
@@ -0,0 +1,151 @@
1
+ """URL validation models with lenient and strict modes.
2
+
3
+ This module provides Pydantic validators for URL strings with two validation modes:
4
+ - lenient: Allows localhost, private IPs, custom ports (for development)
5
+ - strict: RFC-aligned HTTP(S) only (for production)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from enum import Enum
11
+ from typing import Any
12
+
13
+ from pydantic import AnyUrl, HttpUrl, field_validator, BaseModel, ConfigDict
14
+
15
+
16
+ class ValidationMode(str, Enum):
17
+ """URL validation mode."""
18
+
19
+ LENIENT = "lenient"
20
+ STRICT = "strict"
21
+
22
+
23
+ class _ProjectUrlsBase(BaseModel):
24
+ """
25
+ Internal base class for ProjectUrls.
26
+ Provides core functionality and should not be used directly by end-users.
27
+ """
28
+
29
+ model_config = ConfigDict(validate_assignment=True)
30
+
31
+ def to_dict(self) -> dict[str, str]:
32
+ """Returns a dictionary of all resolved URL attributes as strings."""
33
+ return {k: str(v) for k, v in self.model_dump(include=set(self.__class__.model_fields.keys())).items()} # type: ignore[no-any-return]
34
+
35
+ def get_urls(self) -> list[str]:
36
+ """Returns a list of all resolved URL strings."""
37
+ return [str(v) for v in self.to_dict().values()]
38
+
39
+ def __getitem__(self, key: str) -> str:
40
+ """Enables dictionary-style access to URL attributes."""
41
+ if key not in self.__class__.model_fields:
42
+ raise KeyError(f"'{key}' is not a configured URL.")
43
+ value = getattr(self, key)
44
+ return str(value) # type: ignore[no-any-return]
45
+
46
+
47
+ class LenientUrlModel(BaseModel):
48
+ """
49
+ Lenient URL validator that accepts localhost, private IPs, and custom ports.
50
+
51
+ Suitable for development environments where URLs like http://localhost:8000
52
+ are common.
53
+ """
54
+
55
+ model_config = ConfigDict(arbitrary_types_allowed=True)
56
+ url: AnyUrl
57
+
58
+ @field_validator("url", mode="before")
59
+ @classmethod
60
+ def validate_lenient_url(cls, v: Any) -> Any:
61
+ """
62
+ Lenient validation: accept any syntactically valid URL.
63
+
64
+ This includes:
65
+ - localhost and 127.0.0.1
66
+ - Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
67
+ - Custom ports
68
+ - http and https schemes
69
+ """
70
+ if not isinstance(v, str):
71
+ v = str(v)
72
+
73
+ # Basic checks for obviously invalid URLs
74
+ if not v or v.isspace():
75
+ raise ValueError("URL cannot be empty or whitespace")
76
+
77
+ # Let Pydantic's AnyUrl handle the rest of the validation
78
+ # It's permissive enough for development use cases
79
+ return v
80
+
81
+
82
+ class StrictUrlModel(BaseModel):
83
+ """
84
+ Strict URL validator that only accepts RFC-compliant HTTP(S) URLs.
85
+
86
+ Suitable for production environments where URLs must be publicly accessible
87
+ and follow strict HTTP(S) standards.
88
+ """
89
+
90
+ model_config = ConfigDict(arbitrary_types_allowed=True)
91
+ url: HttpUrl
92
+
93
+ @field_validator("url", mode="after")
94
+ @classmethod
95
+ def validate_strict_url(cls, v: HttpUrl) -> HttpUrl:
96
+ """
97
+ Strict validation: only accept HTTP(S) URLs with proper hosts.
98
+
99
+ This rejects:
100
+ - localhost URLs
101
+ - Private IP addresses
102
+ - Non-HTTP(S) schemes
103
+ - Malformed URLs
104
+ """
105
+ # Check for localhost
106
+ host = v.host
107
+ if host in ("localhost", "127.0.0.1", "::1"):
108
+ raise ValueError("localhost URLs are not allowed in strict mode")
109
+
110
+ # Check for private IP ranges
111
+ if host:
112
+ # Check IPv4 private ranges
113
+ if host.startswith("10."):
114
+ raise ValueError("Private IP addresses (10.0.0.0/8) are not allowed in strict mode")
115
+ if host.startswith("172.") and len(host.split(".")) == 4:
116
+ second_octet = int(host.split(".")[1])
117
+ if 16 <= second_octet <= 31:
118
+ raise ValueError("Private IP addresses (172.16.0.0/12) are not allowed in strict mode")
119
+ if host.startswith("192.168."):
120
+ raise ValueError("Private IP addresses (192.168.0.0/16) are not allowed in strict mode")
121
+
122
+ return v
123
+
124
+
125
+ def validate_url(url_string: str, mode: ValidationMode = ValidationMode.LENIENT) -> str:
126
+ """
127
+ Validate a URL string according to the specified mode.
128
+
129
+ Args:
130
+ url_string: The URL string to validate
131
+ mode: Validation mode (lenient or strict)
132
+
133
+ Returns:
134
+ The validated URL as a string
135
+
136
+ Raises:
137
+ ValidationError: If the URL is invalid for the given mode
138
+
139
+ Examples:
140
+ >>> validate_url("http://localhost:8000", ValidationMode.LENIENT)
141
+ 'http://localhost:8000/'
142
+
143
+ >>> validate_url("https://example.com/api", ValidationMode.STRICT)
144
+ 'https://example.com/api'
145
+ """
146
+ if mode == ValidationMode.LENIENT:
147
+ lenient_model = LenientUrlModel(url=url_string) # type: ignore[arg-type]
148
+ return str(lenient_model.url)
149
+ else:
150
+ strict_model = StrictUrlModel(url=url_string) # type: ignore[arg-type]
151
+ return str(strict_model.url)