pydantic-fixturegen 1.0.0__py3-none-any.whl → 1.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.

Potentially problematic release.


This version of pydantic-fixturegen might be problematic. Click here for more details.

Files changed (39) hide show
  1. pydantic_fixturegen/api/__init__.py +137 -0
  2. pydantic_fixturegen/api/_runtime.py +726 -0
  3. pydantic_fixturegen/api/models.py +73 -0
  4. pydantic_fixturegen/cli/__init__.py +32 -1
  5. pydantic_fixturegen/cli/check.py +230 -0
  6. pydantic_fixturegen/cli/diff.py +992 -0
  7. pydantic_fixturegen/cli/doctor.py +188 -35
  8. pydantic_fixturegen/cli/gen/_common.py +134 -7
  9. pydantic_fixturegen/cli/gen/explain.py +597 -40
  10. pydantic_fixturegen/cli/gen/fixtures.py +244 -112
  11. pydantic_fixturegen/cli/gen/json.py +229 -138
  12. pydantic_fixturegen/cli/gen/schema.py +170 -85
  13. pydantic_fixturegen/cli/init.py +333 -0
  14. pydantic_fixturegen/cli/schema.py +45 -0
  15. pydantic_fixturegen/cli/watch.py +126 -0
  16. pydantic_fixturegen/core/config.py +137 -3
  17. pydantic_fixturegen/core/config_schema.py +178 -0
  18. pydantic_fixturegen/core/constraint_report.py +305 -0
  19. pydantic_fixturegen/core/errors.py +42 -0
  20. pydantic_fixturegen/core/field_policies.py +100 -0
  21. pydantic_fixturegen/core/generate.py +241 -37
  22. pydantic_fixturegen/core/io_utils.py +10 -2
  23. pydantic_fixturegen/core/path_template.py +197 -0
  24. pydantic_fixturegen/core/presets.py +73 -0
  25. pydantic_fixturegen/core/providers/temporal.py +10 -0
  26. pydantic_fixturegen/core/safe_import.py +146 -12
  27. pydantic_fixturegen/core/seed_freeze.py +176 -0
  28. pydantic_fixturegen/emitters/json_out.py +65 -16
  29. pydantic_fixturegen/emitters/pytest_codegen.py +68 -13
  30. pydantic_fixturegen/emitters/schema_out.py +27 -3
  31. pydantic_fixturegen/logging.py +114 -0
  32. pydantic_fixturegen/schemas/config.schema.json +244 -0
  33. pydantic_fixturegen-1.1.0.dist-info/METADATA +173 -0
  34. pydantic_fixturegen-1.1.0.dist-info/RECORD +57 -0
  35. pydantic_fixturegen-1.0.0.dist-info/METADATA +0 -280
  36. pydantic_fixturegen-1.0.0.dist-info/RECORD +0 -41
  37. {pydantic_fixturegen-1.0.0.dist-info → pydantic_fixturegen-1.1.0.dist-info}/WHEEL +0 -0
  38. {pydantic_fixturegen-1.0.0.dist-info → pydantic_fixturegen-1.1.0.dist-info}/entry_points.txt +0 -0
  39. {pydantic_fixturegen-1.0.0.dist-info → pydantic_fixturegen-1.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,126 @@
1
+ """Helpers for implementing watch mode across CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Iterable, Iterator
6
+ from pathlib import Path
7
+ from typing import cast
8
+
9
+ from pydantic_fixturegen.core.errors import WatchError
10
+ from pydantic_fixturegen.logging import get_logger
11
+
12
+ _CONFIG_FILENAMES = (
13
+ Path("pyproject.toml"),
14
+ Path("pydantic-fixturegen.yaml"),
15
+ Path("pydantic-fixturegen.yml"),
16
+ )
17
+
18
+
19
+ def gather_default_watch_paths(
20
+ target: Path,
21
+ *,
22
+ output: Path | None = None,
23
+ extra: Iterable[Path] | None = None,
24
+ ) -> list[Path]:
25
+ """Collect filesystem locations to monitor for change events."""
26
+
27
+ paths: set[Path] = set()
28
+ target = target.resolve()
29
+ paths.add(target)
30
+ paths.add(target.parent)
31
+
32
+ cwd = Path.cwd()
33
+ for candidate in _CONFIG_FILENAMES:
34
+ path = (cwd / candidate).resolve()
35
+ if path.exists():
36
+ paths.add(path)
37
+
38
+ if output is not None:
39
+ out_path = output.resolve()
40
+ if out_path.exists():
41
+ paths.add(out_path)
42
+ parent = out_path.parent
43
+ parent.mkdir(parents=True, exist_ok=True)
44
+ paths.add(parent)
45
+
46
+ if extra:
47
+ for candidate in extra:
48
+ resolved = candidate.resolve()
49
+ if resolved.exists():
50
+ paths.add(resolved)
51
+ parent = resolved.parent
52
+ if parent.exists():
53
+ paths.add(parent)
54
+
55
+ normalized = _normalize_watch_paths(paths)
56
+ if not normalized:
57
+ raise WatchError("No valid paths available for watch mode.")
58
+ return normalized
59
+
60
+
61
+ WatchIterator = Iterator[set[tuple[int, Path]]]
62
+ WatchBackend = Callable[..., WatchIterator]
63
+
64
+
65
+ def run_with_watch(
66
+ run_once: Callable[[], None],
67
+ watch_paths: Iterable[Path],
68
+ *,
69
+ debounce: float,
70
+ ) -> None:
71
+ """Execute ``run_once`` immediately and re-run when filesystem changes occur."""
72
+
73
+ watch_fn: WatchBackend = _import_watch_backend()
74
+ normalized = _normalize_watch_paths(watch_paths)
75
+ if not normalized:
76
+ raise WatchError("No valid paths available for watch mode.")
77
+
78
+ run_once()
79
+ logger = get_logger()
80
+ logger.info(
81
+ "Watch mode active. Press Ctrl+C to stop.",
82
+ event="watch_started",
83
+ paths=[str(path) for path in normalized],
84
+ debounce=debounce,
85
+ )
86
+
87
+ try:
88
+ for changes in watch_fn(*normalized, debounce=debounce):
89
+ changed_paths = sorted({str(path) for _, path in changes})
90
+ logger.info(
91
+ "Detected changes",
92
+ event="watch_change_detected",
93
+ paths=changed_paths,
94
+ )
95
+ run_once()
96
+ except KeyboardInterrupt:
97
+ logger.warn("Watch mode stopped.", event="watch_stopped")
98
+
99
+
100
+ def _normalize_watch_paths(paths: Iterable[Path]) -> list[Path]:
101
+ normalized: list[Path] = []
102
+ seen: set[Path] = set()
103
+
104
+ for path in paths:
105
+ candidate = path.resolve()
106
+ monitor = candidate if candidate.exists() else candidate.parent
107
+ if not monitor.exists():
108
+ continue
109
+ if monitor not in seen:
110
+ normalized.append(monitor)
111
+ seen.add(monitor)
112
+ return normalized
113
+
114
+
115
+ def _import_watch_backend() -> WatchBackend:
116
+ try:
117
+ from watchfiles import watch as watch_fn
118
+ except ModuleNotFoundError as exc: # pragma: no cover - depends on optional extra
119
+ raise WatchError(
120
+ "Watch mode requires the optional 'watchfiles' dependency. Install it via"
121
+ " `pip install pydantic-fixturegen[watch]`."
122
+ ) from exc
123
+ return cast(WatchBackend, watch_fn)
124
+
125
+
126
+ __all__ = ["gather_default_watch_paths", "run_with_watch"]
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import datetime
5
6
  import os
6
7
  from collections.abc import Mapping, MutableMapping, Sequence
7
8
  from dataclasses import dataclass, field, replace
@@ -9,6 +10,8 @@ from importlib import import_module
9
10
  from pathlib import Path
10
11
  from typing import Any, TypeVar, cast
11
12
 
13
+ from .field_policies import FieldPolicy
14
+ from .presets import get_preset_spec, normalize_preset_name
12
15
  from .seed import DEFAULT_LOCALE
13
16
 
14
17
 
@@ -62,6 +65,7 @@ class EmittersConfig:
62
65
 
63
66
  @dataclass(frozen=True)
64
67
  class AppConfig:
68
+ preset: str | None = None
65
69
  seed: int | str | None = None
66
70
  locale: str = DEFAULT_LOCALE
67
71
  include: tuple[str, ...] = ()
@@ -69,7 +73,9 @@ class AppConfig:
69
73
  p_none: float | None = None
70
74
  union_policy: str = "first"
71
75
  enum_policy: str = "first"
76
+ now: datetime.datetime | None = None
72
77
  overrides: Mapping[str, Mapping[str, Any]] = field(default_factory=dict)
78
+ field_policies: tuple[FieldPolicy, ...] = ()
73
79
  emitters: EmittersConfig = field(default_factory=EmittersConfig)
74
80
  json: JsonConfig = field(default_factory=JsonConfig)
75
81
 
@@ -96,19 +102,20 @@ def load_config(
96
102
  _deep_merge(data, _config_defaults_dict())
97
103
 
98
104
  file_config = _load_file_config(pyproject, yaml_file)
99
- _deep_merge(data, file_config)
105
+ _merge_source_with_preset(data, file_config)
100
106
 
101
107
  env_config = _load_env_config(env or os.environ)
102
- _deep_merge(data, env_config)
108
+ _merge_source_with_preset(data, env_config)
103
109
 
104
110
  if cli:
105
- _deep_merge(data, cli)
111
+ _merge_source_with_preset(data, cli)
106
112
 
107
113
  return _build_app_config(data)
108
114
 
109
115
 
110
116
  def _config_defaults_dict() -> dict[str, Any]:
111
117
  return {
118
+ "preset": DEFAULT_CONFIG.preset,
112
119
  "seed": DEFAULT_CONFIG.seed,
113
120
  "locale": DEFAULT_CONFIG.locale,
114
121
  "include": list(DEFAULT_CONFIG.include),
@@ -116,6 +123,8 @@ def _config_defaults_dict() -> dict[str, Any]:
116
123
  "p_none": DEFAULT_CONFIG.p_none,
117
124
  "union_policy": DEFAULT_CONFIG.union_policy,
118
125
  "enum_policy": DEFAULT_CONFIG.enum_policy,
126
+ "now": DEFAULT_CONFIG.now,
127
+ "field_policies": {},
119
128
  "overrides": {},
120
129
  "emitters": {
121
130
  "pytest": {
@@ -233,6 +242,8 @@ def _coerce_env_value(value: str) -> Any:
233
242
 
234
243
 
235
244
  def _build_app_config(data: Mapping[str, Any]) -> AppConfig:
245
+ preset_value = _coerce_preset_value(data.get("preset"))
246
+
236
247
  seed = data.get("seed")
237
248
  locale = _coerce_str(data.get("locale"), "locale")
238
249
  include = _normalize_sequence(data.get("include"))
@@ -257,6 +268,8 @@ def _build_app_config(data: Mapping[str, Any]) -> AppConfig:
257
268
 
258
269
  emitters_value = _normalize_emitters(data.get("emitters"))
259
270
  json_value = _normalize_json(data.get("json"))
271
+ field_policies_value = _normalize_field_policies(data.get("field_policies"))
272
+ now_value = _coerce_datetime(data.get("now"), "now")
260
273
 
261
274
  seed_value: int | str | None
262
275
  if isinstance(seed, (int, str)) or seed is None:
@@ -265,6 +278,7 @@ def _build_app_config(data: Mapping[str, Any]) -> AppConfig:
265
278
  raise ConfigError("seed must be an int, str, or null.")
266
279
 
267
280
  config = AppConfig(
281
+ preset=preset_value,
268
282
  seed=seed_value,
269
283
  locale=locale,
270
284
  include=include,
@@ -272,7 +286,9 @@ def _build_app_config(data: Mapping[str, Any]) -> AppConfig:
272
286
  p_none=p_none_value,
273
287
  union_policy=union_policy,
274
288
  enum_policy=enum_policy,
289
+ now=now_value,
275
290
  overrides=overrides_value,
291
+ field_policies=field_policies_value,
276
292
  emitters=emitters_value,
277
293
  json=json_value,
278
294
  )
@@ -315,6 +331,30 @@ def _coerce_policy(value: Any, allowed: set[str], field_name: str) -> str:
315
331
  return value
316
332
 
317
333
 
334
+ def _coerce_datetime(value: Any, field_name: str) -> datetime.datetime | None:
335
+ if value is None:
336
+ return None
337
+ if isinstance(value, datetime.datetime):
338
+ if value.tzinfo is None:
339
+ return value.replace(tzinfo=datetime.timezone.utc)
340
+ return value
341
+ if isinstance(value, datetime.date):
342
+ return datetime.datetime.combine(value, datetime.time(), tzinfo=datetime.timezone.utc)
343
+ if isinstance(value, str):
344
+ text = value.strip()
345
+ if not text or text.lower() == "none":
346
+ return None
347
+ normalized = text.replace("Z", "+00:00")
348
+ try:
349
+ parsed = datetime.datetime.fromisoformat(normalized)
350
+ except ValueError as exc:
351
+ raise ConfigError(f"{field_name} must be an ISO 8601 datetime string.") from exc
352
+ if parsed.tzinfo is None:
353
+ parsed = parsed.replace(tzinfo=datetime.timezone.utc)
354
+ return parsed
355
+ raise ConfigError(f"{field_name} must be an ISO 8601 datetime string or datetime object.")
356
+
357
+
318
358
  def _normalize_overrides(value: Any) -> Mapping[str, Mapping[str, Any]]:
319
359
  if value is None:
320
360
  return {}
@@ -335,6 +375,59 @@ def _normalize_overrides(value: Any) -> Mapping[str, Mapping[str, Any]]:
335
375
  return overrides
336
376
 
337
377
 
378
+ def _normalize_field_policies(value: Any) -> tuple[FieldPolicy, ...]:
379
+ if value is None:
380
+ return ()
381
+ if not isinstance(value, Mapping):
382
+ raise ConfigError("field_policies must be a mapping of pattern to policy settings.")
383
+
384
+ policies: list[FieldPolicy] = []
385
+ for index, (pattern, raw_options) in enumerate(value.items()):
386
+ if not isinstance(pattern, str) or not pattern.strip():
387
+ raise ConfigError("field policy keys must be non-empty strings.")
388
+ if not isinstance(raw_options, Mapping):
389
+ raise ConfigError(f"Field policy '{pattern}' must be a mapping of options.")
390
+
391
+ p_none = raw_options.get("p_none")
392
+ if p_none is not None:
393
+ try:
394
+ p_none = float(p_none)
395
+ except (TypeError, ValueError) as exc:
396
+ raise ConfigError(
397
+ f"Field policy '{pattern}' p_none must be a float value."
398
+ ) from exc
399
+ if not (0.0 <= p_none <= 1.0):
400
+ raise ConfigError(f"Field policy '{pattern}' p_none must be between 0.0 and 1.0.")
401
+
402
+ enum_policy = raw_options.get("enum_policy")
403
+ if enum_policy is not None and (
404
+ not isinstance(enum_policy, str) or enum_policy not in ENUM_POLICIES
405
+ ):
406
+ raise ConfigError(
407
+ f"Field policy '{pattern}' enum_policy must be one of {sorted(ENUM_POLICIES)}."
408
+ )
409
+
410
+ union_policy = raw_options.get("union_policy")
411
+ if union_policy is not None and (
412
+ not isinstance(union_policy, str) or union_policy not in UNION_POLICIES
413
+ ):
414
+ raise ConfigError(
415
+ f"Field policy '{pattern}' union_policy must be one of {sorted(UNION_POLICIES)}."
416
+ )
417
+
418
+ allowed_keys = {"p_none", "enum_policy", "union_policy"}
419
+ for option_key in raw_options:
420
+ if option_key not in allowed_keys:
421
+ raise ConfigError(
422
+ f"Field policy '{pattern}' contains unsupported option '{option_key}'."
423
+ )
424
+
425
+ options = {"p_none": p_none, "enum_policy": enum_policy, "union_policy": union_policy}
426
+ policies.append(FieldPolicy(pattern=pattern, options=options, index=index))
427
+
428
+ return tuple(policies)
429
+
430
+
338
431
  def _normalize_emitters(value: Any) -> EmittersConfig:
339
432
  pytest_config = PytestEmitterConfig()
340
433
 
@@ -409,6 +502,22 @@ def _coerce_optional_str(value: Any, field_name: str) -> str:
409
502
  return value
410
503
 
411
504
 
505
+ def _coerce_preset_value(value: Any) -> str | None:
506
+ if value is None:
507
+ return None
508
+ if not isinstance(value, str):
509
+ raise ConfigError("preset must be a string when specified.")
510
+ stripped = value.strip()
511
+ if not stripped:
512
+ return None
513
+ normalized = normalize_preset_name(stripped)
514
+ try:
515
+ spec = get_preset_spec(normalized)
516
+ except KeyError as exc:
517
+ raise ConfigError(f"Unknown preset '{value}'.") from exc
518
+ return spec.name
519
+
520
+
412
521
  def _ensure_mutable(mapping: Mapping[str, Any]) -> dict[str, Any]:
413
522
  mutable: dict[str, Any] = {}
414
523
  for key, value in mapping.items():
@@ -438,3 +547,28 @@ def _deep_merge(target: MutableMapping[str, Any], source: Mapping[str, Any]) ->
438
547
  target[key] = list(value)
439
548
  else:
440
549
  target[key] = value
550
+
551
+
552
+ def _merge_source_with_preset(data: MutableMapping[str, Any], source: Mapping[str, Any]) -> None:
553
+ if not source:
554
+ return
555
+
556
+ mutable = _ensure_mutable(source)
557
+
558
+ if "preset" in mutable:
559
+ preset_raw = mutable.pop("preset")
560
+ if preset_raw is None:
561
+ data["preset"] = None
562
+ else:
563
+ if not isinstance(preset_raw, str):
564
+ raise ConfigError("preset must be a string when specified.")
565
+ preset_name = normalize_preset_name(preset_raw)
566
+ try:
567
+ preset_spec = get_preset_spec(preset_name)
568
+ except KeyError as exc:
569
+ raise ConfigError(f"Unknown preset '{preset_raw}'.") from exc
570
+
571
+ _deep_merge(data, _ensure_mutable(preset_spec.settings))
572
+ data["preset"] = preset_spec.name
573
+
574
+ _deep_merge(data, mutable)
@@ -0,0 +1,178 @@
1
+ """JSON Schema generation for pydantic-fixturegen configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Literal, cast
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ from .config import DEFAULT_CONFIG
10
+ from .seed import DEFAULT_LOCALE
11
+
12
+ SCHEMA_ID = "https://raw.githubusercontent.com/CasperKristiansson/pydantic-fixturegen/main/pydantic_fixturegen/schemas/config.schema.json"
13
+ SCHEMA_DRAFT = "https://json-schema.org/draft/2020-12/schema"
14
+
15
+
16
+ UnionPolicyLiteral = Literal["first", "random", "weighted"]
17
+ EnumPolicyLiteral = Literal["first", "random"]
18
+
19
+ DEFAULT_PYTEST_STYLE = cast(
20
+ Literal["functions", "factory", "class"],
21
+ DEFAULT_CONFIG.emitters.pytest.style,
22
+ )
23
+ DEFAULT_PYTEST_SCOPE = cast(
24
+ Literal["function", "module", "session"],
25
+ DEFAULT_CONFIG.emitters.pytest.scope,
26
+ )
27
+ DEFAULT_UNION_POLICY = cast(UnionPolicyLiteral, DEFAULT_CONFIG.union_policy)
28
+ DEFAULT_ENUM_POLICY = cast(EnumPolicyLiteral, DEFAULT_CONFIG.enum_policy)
29
+
30
+
31
+ class PytestEmitterSchema(BaseModel):
32
+ """Schema for pytest emitter settings."""
33
+
34
+ model_config = ConfigDict(extra="forbid")
35
+
36
+ style: Literal["functions", "factory", "class"] = Field(
37
+ default=DEFAULT_PYTEST_STYLE,
38
+ description="How emitted pytest fixtures are structured.",
39
+ )
40
+ scope: Literal["function", "module", "session"] = Field(
41
+ default=DEFAULT_PYTEST_SCOPE,
42
+ description="Default pytest fixture scope.",
43
+ )
44
+
45
+
46
+ class EmittersSchema(BaseModel):
47
+ """Schema for emitter configuration sections."""
48
+
49
+ model_config = ConfigDict(extra="forbid")
50
+
51
+ pytest: PytestEmitterSchema = Field(
52
+ default_factory=PytestEmitterSchema,
53
+ description="Configuration for the built-in pytest fixture emitter.",
54
+ )
55
+
56
+
57
+ class JsonSchema(BaseModel):
58
+ """Schema for JSON emitter settings."""
59
+
60
+ model_config = ConfigDict(extra="forbid")
61
+
62
+ indent: int = Field(
63
+ default=DEFAULT_CONFIG.json.indent,
64
+ ge=0,
65
+ description="Indentation level for JSON output (0 for compact).",
66
+ )
67
+ orjson: bool = Field(
68
+ default=DEFAULT_CONFIG.json.orjson,
69
+ description="Use orjson for serialization when available.",
70
+ )
71
+
72
+
73
+ class FieldPolicyOptionsSchema(BaseModel):
74
+ """Schema describing supported field policy overrides."""
75
+
76
+ model_config = ConfigDict(extra="forbid")
77
+
78
+ p_none: float | None = Field(
79
+ default=None,
80
+ ge=0.0,
81
+ le=1.0,
82
+ description="Probability override for returning None on optional fields.",
83
+ )
84
+ enum_policy: EnumPolicyLiteral | None = Field(
85
+ default=None,
86
+ description="Enum selection policy for matching fields.",
87
+ )
88
+ union_policy: Literal["first", "random"] | None = Field(
89
+ default=None,
90
+ description="Union selection policy for matching fields.",
91
+ )
92
+
93
+
94
+ class ConfigSchemaModel(BaseModel):
95
+ """Authoritative schema for `[tool.pydantic_fixturegen]` configuration."""
96
+
97
+ model_config = ConfigDict(
98
+ extra="allow",
99
+ populate_by_name=True,
100
+ title="pydantic-fixturegen configuration",
101
+ )
102
+
103
+ preset: str | None = Field(
104
+ default=DEFAULT_CONFIG.preset,
105
+ description="Curated preset name applied before other configuration (e.g., 'boundary').",
106
+ )
107
+ seed: int | str | None = Field(
108
+ default=DEFAULT_CONFIG.seed,
109
+ description="Global seed controlling deterministic generation. Accepts int or string.",
110
+ )
111
+ locale: str = Field(
112
+ default=DEFAULT_LOCALE,
113
+ description="Default Faker locale used when generating data.",
114
+ )
115
+ include: list[str] = Field(
116
+ default_factory=list,
117
+ description="Glob patterns of fully-qualified model names to include by default.",
118
+ )
119
+ exclude: list[str] = Field(
120
+ default_factory=list,
121
+ description="Glob patterns of fully-qualified model names to exclude by default.",
122
+ )
123
+ p_none: float | None = Field(
124
+ default=DEFAULT_CONFIG.p_none,
125
+ ge=0.0,
126
+ le=1.0,
127
+ description="Probability of sampling `None` for optional fields when unspecified.",
128
+ )
129
+ union_policy: UnionPolicyLiteral = Field(
130
+ default=DEFAULT_UNION_POLICY,
131
+ description="Strategy for selecting branches of `typing.Union`.",
132
+ )
133
+ enum_policy: EnumPolicyLiteral = Field(
134
+ default=DEFAULT_ENUM_POLICY,
135
+ description="Strategy for selecting enum members.",
136
+ )
137
+ overrides: dict[str, dict[str, Any]] = Field(
138
+ default_factory=dict,
139
+ description="Per-model overrides keyed by fully-qualified model name.",
140
+ )
141
+ emitters: EmittersSchema = Field(
142
+ default_factory=EmittersSchema,
143
+ description="Emitter-specific configuration sections.",
144
+ )
145
+ json_settings: JsonSchema = Field(
146
+ default_factory=JsonSchema,
147
+ alias="json",
148
+ description="Settings shared by JSON-based emitters.",
149
+ )
150
+ field_policies: dict[str, FieldPolicyOptionsSchema] = Field(
151
+ default_factory=dict,
152
+ description=(
153
+ "Field policy definitions keyed by glob or regex patterns that may target model "
154
+ "names or dotted field paths."
155
+ ),
156
+ )
157
+
158
+
159
+ def build_config_schema() -> dict[str, Any]:
160
+ """Return the JSON schema describing the project configuration."""
161
+
162
+ schema = ConfigSchemaModel.model_json_schema(by_alias=True)
163
+ schema["$schema"] = SCHEMA_DRAFT
164
+ schema["$id"] = SCHEMA_ID
165
+ schema.setdefault("description", "Configuration options for pydantic-fixturegen")
166
+ return schema
167
+
168
+
169
+ def get_config_schema_json(*, indent: int | None = 2) -> str:
170
+ """Return the configuration schema serialized to JSON."""
171
+
172
+ import json
173
+
174
+ schema = build_config_schema()
175
+ return json.dumps(schema, indent=indent, sort_keys=True) + ("\n" if indent else "")
176
+
177
+
178
+ __all__ = ["build_config_schema", "get_config_schema_json", "SCHEMA_ID", "SCHEMA_DRAFT"]