sqlspec 0.3.0__py3-none-any.whl → 0.5.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 sqlspec might be problematic. Click here for more details.

sqlspec/typing.py CHANGED
@@ -1,6 +1,7 @@
1
- from __future__ import annotations
1
+ from __future__ import annotations # noqa: A005
2
2
 
3
3
  from collections.abc import Sequence
4
+ from dataclasses import Field, fields
4
5
  from functools import lru_cache
5
6
  from typing import (
6
7
  TYPE_CHECKING,
@@ -18,15 +19,22 @@ from sqlspec._typing import (
18
19
  PYDANTIC_INSTALLED,
19
20
  UNSET,
20
21
  BaseModel,
22
+ DataclassProtocol,
23
+ Empty,
24
+ EmptyType,
21
25
  FailFast,
22
26
  Struct,
23
27
  TypeAdapter,
28
+ UnsetType,
24
29
  convert,
25
30
  )
26
- from sqlspec.utils.dataclass import DataclassProtocol, is_dataclass_instance, simple_asdict
27
31
 
28
32
  if TYPE_CHECKING:
29
- from .filters import StatementFilter
33
+ from collections.abc import Iterable
34
+ from collections.abc import Set as AbstractSet
35
+
36
+ from sqlspec.filters import StatementFilter
37
+
30
38
 
31
39
  PYDANTIC_USE_FAILFAST = False # leave permanently disabled for now
32
40
 
@@ -40,11 +48,21 @@ FilterTypeT = TypeVar("FilterTypeT", bound="StatementFilter")
40
48
 
41
49
  :class:`~advanced_alchemy.filters.StatementFilter`
42
50
  """
51
+ ModelDTOT = TypeVar("ModelDTOT", bound="Struct | BaseModel")
52
+ """Type variable for model DTOs.
53
+
54
+ :class:`msgspec.Struct`|:class:`pydantic.BaseModel`
55
+ """
56
+ PydanticOrMsgspecT: TypeAlias = Union[Struct, BaseModel]
57
+ """Type alias for pydantic or msgspec models.
58
+
59
+ :class:`msgspec.Struct` or :class:`pydantic.BaseModel`
60
+ """
43
61
  ModelDictT: TypeAlias = Union[dict[str, Any], ModelT, DataclassProtocol, Struct, BaseModel]
44
62
  """Type alias for model dictionaries.
45
63
 
46
64
  Represents:
47
- - :type:`dict[str, Any]` | :class:`~advanced_alchemy.base.ModelProtocol` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel` | :class:`litestar.dto.data_structures.DTOData` | :class:`~advanced_alchemy.base.ModelProtocol`
65
+ - :type:`dict[str, Any]` | :class:`msgspec.Struct` | :class:`pydantic.BaseModel`
48
66
  """
49
67
  ModelDictListT: TypeAlias = Sequence[Union[dict[str, Any], ModelT, DataclassProtocol, Struct, BaseModel]]
50
68
  """Type alias for model dictionary lists.
@@ -55,6 +73,18 @@ A list or sequence of any of the following:
55
73
  """
56
74
 
57
75
 
76
+ def is_dataclass_instance(obj: Any) -> TypeGuard[DataclassProtocol]:
77
+ """Check if an object is a dataclass instance.
78
+
79
+ Args:
80
+ obj: An object to check.
81
+
82
+ Returns:
83
+ True if the object is a dataclass instance.
84
+ """
85
+ return hasattr(type(obj), "__dataclass_fields__") # pyright: ignore[reportUnknownArgumentType]
86
+
87
+
58
88
  @lru_cache(typed=True)
59
89
  def get_type_adapter(f: type[T]) -> TypeAdapter[T]:
60
90
  """Caches and returns a pydantic type adapter.
@@ -84,7 +114,33 @@ def is_pydantic_model(v: Any) -> TypeGuard[BaseModel]:
84
114
  return PYDANTIC_INSTALLED and isinstance(v, BaseModel)
85
115
 
86
116
 
87
- def is_msgspec_model(v: Any) -> TypeGuard[Struct]:
117
+ def is_pydantic_model_with_field(v: Any, field_name: str) -> TypeGuard[BaseModel]:
118
+ """Check if a pydantic model has a specific field.
119
+
120
+ Args:
121
+ v: Value to check.
122
+ field_name: Field name to check for.
123
+
124
+ Returns:
125
+ bool
126
+ """
127
+ return is_pydantic_model(v) and field_name in v.model_fields
128
+
129
+
130
+ def is_pydantic_model_without_field(v: Any, field_name: str) -> TypeGuard[BaseModel]:
131
+ """Check if a pydantic model does not have a specific field.
132
+
133
+ Args:
134
+ v: Value to check.
135
+ field_name: Field name to check for.
136
+
137
+ Returns:
138
+ bool
139
+ """
140
+ return not is_pydantic_model_with_field(v, field_name)
141
+
142
+
143
+ def is_msgspec_struct(v: Any) -> TypeGuard[Struct]:
88
144
  """Check if a value is a msgspec model.
89
145
 
90
146
  Args:
@@ -96,6 +152,32 @@ def is_msgspec_model(v: Any) -> TypeGuard[Struct]:
96
152
  return MSGSPEC_INSTALLED and isinstance(v, Struct)
97
153
 
98
154
 
155
+ def is_msgspec_struct_with_field(v: Any, field_name: str) -> TypeGuard[Struct]:
156
+ """Check if a msgspec model has a specific field.
157
+
158
+ Args:
159
+ v: Value to check.
160
+ field_name: Field name to check for.
161
+
162
+ Returns:
163
+ bool
164
+ """
165
+ return is_msgspec_struct(v) and field_name in v.__struct_fields__
166
+
167
+
168
+ def is_msgspec_struct_without_field(v: Any, field_name: str) -> TypeGuard[Struct]:
169
+ """Check if a msgspec model does not have a specific field.
170
+
171
+ Args:
172
+ v: Value to check.
173
+ field_name: Field name to check for.
174
+
175
+ Returns:
176
+ bool
177
+ """
178
+ return not is_msgspec_struct_with_field(v, field_name)
179
+
180
+
99
181
  def is_dict(v: Any) -> TypeGuard[dict[str, Any]]:
100
182
  """Check if a value is a dictionary.
101
183
 
@@ -134,8 +216,8 @@ def is_dict_without_field(v: Any, field_name: str) -> TypeGuard[dict[str, Any]]:
134
216
  return is_dict(v) and field_name not in v
135
217
 
136
218
 
137
- def is_dataclass(v: Any) -> TypeGuard[DataclassProtocol]:
138
- """Check if a value is a dataclass.
219
+ def is_schema(v: Any) -> TypeGuard[Struct | BaseModel]:
220
+ """Check if a value is a msgspec Struct or Pydantic model.
139
221
 
140
222
  Args:
141
223
  v: Value to check.
@@ -143,11 +225,23 @@ def is_dataclass(v: Any) -> TypeGuard[DataclassProtocol]:
143
225
  Returns:
144
226
  bool
145
227
  """
146
- return is_dataclass_instance(v)
228
+ return is_msgspec_struct(v) or is_pydantic_model(v)
147
229
 
148
230
 
149
- def is_dataclass_with_field(v: Any, field_name: str) -> TypeGuard[DataclassProtocol]:
150
- """Check if a dataclass has a specific field.
231
+ def is_schema_or_dict(v: Any) -> TypeGuard[Struct | BaseModel | dict[str, Any]]:
232
+ """Check if a value is a msgspec Struct, Pydantic model, or dict.
233
+
234
+ Args:
235
+ v: Value to check.
236
+
237
+ Returns:
238
+ bool
239
+ """
240
+ return is_schema(v) or is_dict(v)
241
+
242
+
243
+ def is_schema_with_field(v: Any, field_name: str) -> TypeGuard[Struct | BaseModel]:
244
+ """Check if a value is a msgspec Struct or Pydantic model with a specific field.
151
245
 
152
246
  Args:
153
247
  v: Value to check.
@@ -156,11 +250,11 @@ def is_dataclass_with_field(v: Any, field_name: str) -> TypeGuard[DataclassProto
156
250
  Returns:
157
251
  bool
158
252
  """
159
- return is_dataclass(v) and field_name in v.__dataclass_fields__
253
+ return is_msgspec_struct_with_field(v, field_name) or is_pydantic_model_with_field(v, field_name)
160
254
 
161
255
 
162
- def is_dataclass_without_field(v: Any, field_name: str) -> TypeGuard[DataclassProtocol]:
163
- """Check if a dataclass does not have a specific field.
256
+ def is_schema_without_field(v: Any, field_name: str) -> TypeGuard[Struct | BaseModel]:
257
+ """Check if a value is a msgspec Struct or Pydantic model without a specific field.
164
258
 
165
259
  Args:
166
260
  v: Value to check.
@@ -169,11 +263,11 @@ def is_dataclass_without_field(v: Any, field_name: str) -> TypeGuard[DataclassPr
169
263
  Returns:
170
264
  bool
171
265
  """
172
- return is_dataclass(v) and field_name not in v.__dataclass_fields__
266
+ return not is_schema_with_field(v, field_name)
173
267
 
174
268
 
175
- def is_pydantic_model_with_field(v: Any, field_name: str) -> TypeGuard[BaseModel]:
176
- """Check if a pydantic model has a specific field.
269
+ def is_schema_or_dict_with_field(v: Any, field_name: str) -> TypeGuard[Struct | BaseModel | dict[str, Any]]:
270
+ """Check if a value is a msgspec Struct, Pydantic model, or dict with a specific field.
177
271
 
178
272
  Args:
179
273
  v: Value to check.
@@ -182,11 +276,11 @@ def is_pydantic_model_with_field(v: Any, field_name: str) -> TypeGuard[BaseModel
182
276
  Returns:
183
277
  bool
184
278
  """
185
- return is_pydantic_model(v) and field_name in v.model_fields
279
+ return is_schema_with_field(v, field_name) or is_dict_with_field(v, field_name)
186
280
 
187
281
 
188
- def is_pydantic_model_without_field(v: Any, field_name: str) -> TypeGuard[BaseModel]:
189
- """Check if a pydantic model does not have a specific field.
282
+ def is_schema_or_dict_without_field(v: Any, field_name: str) -> TypeGuard[Struct | BaseModel | dict[str, Any]]:
283
+ """Check if a value is a msgspec Struct, Pydantic model, or dict without a specific field.
190
284
 
191
285
  Args:
192
286
  v: Value to check.
@@ -195,11 +289,23 @@ def is_pydantic_model_without_field(v: Any, field_name: str) -> TypeGuard[BaseMo
195
289
  Returns:
196
290
  bool
197
291
  """
198
- return not is_pydantic_model_with_field(v, field_name)
292
+ return not is_schema_or_dict_with_field(v, field_name)
199
293
 
200
294
 
201
- def is_msgspec_model_with_field(v: Any, field_name: str) -> TypeGuard[Struct]:
202
- """Check if a msgspec model has a specific field.
295
+ def is_dataclass(v: Any) -> TypeGuard[DataclassProtocol]:
296
+ """Check if a value is a dataclass.
297
+
298
+ Args:
299
+ v: Value to check.
300
+
301
+ Returns:
302
+ bool
303
+ """
304
+ return is_dataclass_instance(v)
305
+
306
+
307
+ def is_dataclass_with_field(v: Any, field_name: str) -> TypeGuard[DataclassProtocol]:
308
+ """Check if a dataclass has a specific field.
203
309
 
204
310
  Args:
205
311
  v: Value to check.
@@ -208,11 +314,11 @@ def is_msgspec_model_with_field(v: Any, field_name: str) -> TypeGuard[Struct]:
208
314
  Returns:
209
315
  bool
210
316
  """
211
- return is_msgspec_model(v) and field_name in v.__struct_fields__
317
+ return is_dataclass(v) and field_name in v.__dataclass_fields__
212
318
 
213
319
 
214
- def is_msgspec_model_without_field(v: Any, field_name: str) -> TypeGuard[Struct]:
215
- """Check if a msgspec model does not have a specific field.
320
+ def is_dataclass_without_field(v: Any, field_name: str) -> TypeGuard[DataclassProtocol]:
321
+ """Check if a dataclass does not have a specific field.
216
322
 
217
323
  Args:
218
324
  v: Value to check.
@@ -221,7 +327,105 @@ def is_msgspec_model_without_field(v: Any, field_name: str) -> TypeGuard[Struct]
221
327
  Returns:
222
328
  bool
223
329
  """
224
- return not is_msgspec_model_with_field(v, field_name)
330
+ return is_dataclass(v) and field_name not in v.__dataclass_fields__
331
+
332
+
333
+ def extract_dataclass_fields(
334
+ dt: DataclassProtocol,
335
+ exclude_none: bool = False,
336
+ exclude_empty: bool = False,
337
+ include: AbstractSet[str] | None = None,
338
+ exclude: AbstractSet[str] | None = None,
339
+ ) -> tuple[Field[Any], ...]:
340
+ """Extract dataclass fields.
341
+
342
+ Args:
343
+ dt: A dataclass instance.
344
+ exclude_none: Whether to exclude None values.
345
+ exclude_empty: Whether to exclude Empty values.
346
+ include: An iterable of fields to include.
347
+ exclude: An iterable of fields to exclude.
348
+
349
+
350
+ Returns:
351
+ A tuple of dataclass fields.
352
+ """
353
+ include = include or set()
354
+ exclude = exclude or set()
355
+
356
+ if common := (include & exclude):
357
+ msg = f"Fields {common} are both included and excluded."
358
+ raise ValueError(msg)
359
+
360
+ dataclass_fields: Iterable[Field[Any]] = fields(dt)
361
+ if exclude_none:
362
+ dataclass_fields = (field for field in dataclass_fields if getattr(dt, field.name) is not None)
363
+ if exclude_empty:
364
+ dataclass_fields = (field for field in dataclass_fields if getattr(dt, field.name) is not Empty)
365
+ if include:
366
+ dataclass_fields = (field for field in dataclass_fields if field.name in include)
367
+ if exclude:
368
+ dataclass_fields = (field for field in dataclass_fields if field.name not in exclude)
369
+
370
+ return tuple(dataclass_fields)
371
+
372
+
373
+ def extract_dataclass_items(
374
+ dt: DataclassProtocol,
375
+ exclude_none: bool = False,
376
+ exclude_empty: bool = False,
377
+ include: AbstractSet[str] | None = None,
378
+ exclude: AbstractSet[str] | None = None,
379
+ ) -> tuple[tuple[str, Any], ...]:
380
+ """Extract dataclass name, value pairs.
381
+
382
+ Unlike the 'asdict' method exports by the stdlib, this function does not pickle values.
383
+
384
+ Args:
385
+ dt: A dataclass instance.
386
+ exclude_none: Whether to exclude None values.
387
+ exclude_empty: Whether to exclude Empty values.
388
+ include: An iterable of fields to include.
389
+ exclude: An iterable of fields to exclude.
390
+
391
+ Returns:
392
+ A tuple of key/value pairs.
393
+ """
394
+ dataclass_fields = extract_dataclass_fields(dt, exclude_none, exclude_empty, include, exclude)
395
+ return tuple((field.name, getattr(dt, field.name)) for field in dataclass_fields)
396
+
397
+
398
+ def dataclass_to_dict(
399
+ obj: DataclassProtocol,
400
+ exclude_none: bool = False,
401
+ exclude_empty: bool = False,
402
+ convert_nested: bool = True,
403
+ exclude: set[str] | None = None,
404
+ ) -> dict[str, Any]:
405
+ """Convert a dataclass to a dictionary.
406
+
407
+ This method has important differences to the standard library version:
408
+ - it does not deepcopy values
409
+ - it does not recurse into collections
410
+
411
+ Args:
412
+ obj: A dataclass instance.
413
+ exclude_none: Whether to exclude None values.
414
+ exclude_empty: Whether to exclude Empty values.
415
+ convert_nested: Whether to recursively convert nested dataclasses.
416
+ exclude: An iterable of fields to exclude.
417
+
418
+ Returns:
419
+ A dictionary of key/value pairs.
420
+ """
421
+ ret = {}
422
+ for field in extract_dataclass_fields(obj, exclude_none, exclude_empty, exclude=exclude):
423
+ value = getattr(obj, field.name)
424
+ if is_dataclass_instance(value) and convert_nested:
425
+ ret[field.name] = dataclass_to_dict(value, exclude_none, exclude_empty)
426
+ else:
427
+ ret[field.name] = getattr(obj, field.name)
428
+ return cast("dict[str, Any]", ret)
225
429
 
226
430
 
227
431
  def schema_dump(
@@ -237,13 +441,15 @@ def schema_dump(
237
441
  Returns:
238
442
  :type: dict[str, Any]
239
443
  """
444
+ if is_dict(data):
445
+ return data
240
446
  if is_dataclass(data):
241
- return simple_asdict(data, exclude_empty=exclude_unset)
447
+ return dataclass_to_dict(data, exclude_empty=exclude_unset)
242
448
  if is_pydantic_model(data):
243
449
  return data.model_dump(exclude_unset=exclude_unset)
244
- if is_msgspec_model(data) and exclude_unset:
450
+ if is_msgspec_struct(data) and exclude_unset:
245
451
  return {f: val for f in data.__struct_fields__ if (val := getattr(data, f, None)) != UNSET}
246
- if is_msgspec_model(data) and not exclude_unset:
452
+ if is_msgspec_struct(data) and not exclude_unset:
247
453
  return {f: getattr(data, f, None) for f in data.__struct_fields__}
248
454
  return cast("dict[str,Any]", data)
249
455
 
@@ -254,6 +460,9 @@ __all__ = (
254
460
  "PYDANTIC_USE_FAILFAST",
255
461
  "UNSET",
256
462
  "BaseModel",
463
+ "DataclassProtocol",
464
+ "Empty",
465
+ "EmptyType",
257
466
  "FailFast",
258
467
  "FilterTypeT",
259
468
  "ModelDictListT",
@@ -262,13 +471,20 @@ __all__ = (
262
471
  "TypeAdapter",
263
472
  "UnsetType",
264
473
  "convert",
474
+ "dataclass_to_dict",
475
+ "extract_dataclass_fields",
476
+ "extract_dataclass_items",
265
477
  "get_type_adapter",
478
+ "is_dataclass",
479
+ "is_dataclass_instance",
480
+ "is_dataclass_with_field",
481
+ "is_dataclass_without_field",
266
482
  "is_dict",
267
483
  "is_dict_with_field",
268
484
  "is_dict_without_field",
269
- "is_msgspec_model",
270
- "is_msgspec_model_with_field",
271
- "is_msgspec_model_without_field",
485
+ "is_msgspec_struct",
486
+ "is_msgspec_struct_with_field",
487
+ "is_msgspec_struct_without_field",
272
488
  "is_pydantic_model",
273
489
  "is_pydantic_model_with_field",
274
490
  "is_pydantic_model_without_field",
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from functools import wraps
5
+ from typing import Callable, Literal
6
+ from warnings import warn
7
+
8
+ from typing_extensions import ParamSpec, TypeVar
9
+
10
+ __all__ = ("deprecated", "warn_deprecation")
11
+
12
+
13
+ T = TypeVar("T")
14
+ P = ParamSpec("P")
15
+ DeprecatedKind = Literal["function", "method", "classmethod", "attribute", "property", "class", "parameter", "import"]
16
+
17
+
18
+ def warn_deprecation(
19
+ version: str,
20
+ deprecated_name: str,
21
+ kind: DeprecatedKind,
22
+ *,
23
+ removal_in: str | None = None,
24
+ alternative: str | None = None,
25
+ info: str | None = None,
26
+ pending: bool = False,
27
+ ) -> None:
28
+ """Warn about a call to a (soon to be) deprecated function.
29
+
30
+ Args:
31
+ version: Advanced Alchemy version where the deprecation will occur
32
+ deprecated_name: Name of the deprecated function
33
+ removal_in: Advanced Alchemy version where the deprecated function will be removed
34
+ alternative: Name of a function that should be used instead
35
+ info: Additional information
36
+ pending: Use :class:`warnings.PendingDeprecationWarning` instead of :class:`warnings.DeprecationWarning`
37
+ kind: Type of the deprecated thing
38
+ """
39
+ parts = []
40
+
41
+ if kind == "import":
42
+ access_type = "Import of"
43
+ elif kind in {"function", "method"}:
44
+ access_type = "Call to"
45
+ else:
46
+ access_type = "Use of"
47
+
48
+ if pending:
49
+ parts.append(f"{access_type} {kind} awaiting deprecation {deprecated_name!r}") # pyright: ignore[reportUnknownMemberType]
50
+ else:
51
+ parts.append(f"{access_type} deprecated {kind} {deprecated_name!r}") # pyright: ignore[reportUnknownMemberType]
52
+
53
+ parts.extend( # pyright: ignore[reportUnknownMemberType]
54
+ (
55
+ f"Deprecated in advanced-alchemy {version}",
56
+ f"This {kind} will be removed in {removal_in or 'the next major version'}",
57
+ ),
58
+ )
59
+ if alternative:
60
+ parts.append(f"Use {alternative!r} instead") # pyright: ignore[reportUnknownMemberType]
61
+
62
+ if info:
63
+ parts.append(info) # pyright: ignore[reportUnknownMemberType]
64
+
65
+ text = ". ".join(parts) # pyright: ignore[reportUnknownArgumentType]
66
+ warning_class = PendingDeprecationWarning if pending else DeprecationWarning
67
+
68
+ warn(text, warning_class, stacklevel=2)
69
+
70
+
71
+ def deprecated(
72
+ version: str,
73
+ *,
74
+ removal_in: str | None = None,
75
+ alternative: str | None = None,
76
+ info: str | None = None,
77
+ pending: bool = False,
78
+ kind: Literal["function", "method", "classmethod", "property"] | None = None,
79
+ ) -> Callable[[Callable[P, T]], Callable[P, T]]:
80
+ """Create a decorator wrapping a function, method or property with a warning call about a (pending) deprecation.
81
+
82
+ Args:
83
+ version: Litestar version where the deprecation will occur
84
+ removal_in: Litestar version where the deprecated function will be removed
85
+ alternative: Name of a function that should be used instead
86
+ info: Additional information
87
+ pending: Use :class:`warnings.PendingDeprecationWarning` instead of :class:`warnings.DeprecationWarning`
88
+ kind: Type of the deprecated callable. If ``None``, will use ``inspect`` to figure
89
+ out if it's a function or method
90
+
91
+ Returns:
92
+ A decorator wrapping the function call with a warning
93
+ """
94
+
95
+ def decorator(func: Callable[P, T]) -> Callable[P, T]:
96
+ @wraps(func)
97
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
98
+ warn_deprecation(
99
+ version=version,
100
+ deprecated_name=func.__name__,
101
+ info=info,
102
+ alternative=alternative,
103
+ pending=pending,
104
+ removal_in=removal_in,
105
+ kind=kind or ("method" if inspect.ismethod(func) else "function"),
106
+ )
107
+ return func(*args, **kwargs)
108
+
109
+ return wrapped
110
+
111
+ return decorator
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from sqlspec._serialization import decode_json
6
+ from sqlspec.exceptions import MissingDependencyError
7
+
8
+ if TYPE_CHECKING:
9
+ from pathlib import Path
10
+
11
+ from anyio import Path as AsyncPath
12
+
13
+ __all__ = ("open_fixture", "open_fixture_async")
14
+
15
+
16
+ def open_fixture(fixtures_path: Path | AsyncPath, fixture_name: str) -> Any:
17
+ """Loads JSON file with the specified fixture name
18
+
19
+ Args:
20
+ fixtures_path: :class:`pathlib.Path` | :class:`anyio.Path` The path to look for fixtures
21
+ fixture_name (str): The fixture name to load.
22
+
23
+ Raises:
24
+ :class:`FileNotFoundError`: Fixtures not found.
25
+
26
+ Returns:
27
+ Any: The parsed JSON data
28
+ """
29
+ from pathlib import Path
30
+
31
+ fixture = Path(fixtures_path / f"{fixture_name}.json")
32
+ if fixture.exists():
33
+ with fixture.open(mode="r", encoding="utf-8") as f:
34
+ f_data = f.read()
35
+ return decode_json(f_data)
36
+ msg = f"Could not find the {fixture_name} fixture"
37
+ raise FileNotFoundError(msg)
38
+
39
+
40
+ async def open_fixture_async(fixtures_path: Path | AsyncPath, fixture_name: str) -> Any:
41
+ """Loads JSON file with the specified fixture name
42
+
43
+ Args:
44
+ fixtures_path: :class:`pathlib.Path` | :class:`anyio.Path` The path to look for fixtures
45
+ fixture_name (str): The fixture name to load.
46
+
47
+ Raises:
48
+ :class:`~advanced_alchemy.exceptions.MissingDependencyError`: The `anyio` library is required to use this function.
49
+ :class:`FileNotFoundError`: Fixtures not found.
50
+
51
+ Returns:
52
+ Any: The parsed JSON data
53
+ """
54
+ try:
55
+ from anyio import Path as AsyncPath
56
+ except ImportError as exc:
57
+ msg = "The `anyio` library is required to use this function. Please install it with `pip install anyio`."
58
+ raise MissingDependencyError(msg) from exc
59
+
60
+ fixture = AsyncPath(fixtures_path / f"{fixture_name}.json")
61
+ if await fixture.exists():
62
+ async with await fixture.open(mode="r", encoding="utf-8") as f:
63
+ f_data = await f.read()
64
+ return decode_json(f_data)
65
+ msg = f"Could not find the {fixture_name} fixture"
66
+ raise FileNotFoundError(msg)
@@ -0,0 +1,94 @@
1
+ """General utility functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from importlib import import_module
7
+ from importlib.util import find_spec
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ if TYPE_CHECKING:
12
+ from types import ModuleType
13
+
14
+ __all__ = (
15
+ "import_string",
16
+ "module_to_os_path",
17
+ )
18
+
19
+
20
+ def module_to_os_path(dotted_path: str = "app") -> Path:
21
+ """Find Module to OS Path.
22
+
23
+ Return a path to the base directory of the project or the module
24
+ specified by `dotted_path`.
25
+
26
+ Args:
27
+ dotted_path: The path to the module. Defaults to "app".
28
+
29
+ Raises:
30
+ TypeError: The module could not be found.
31
+
32
+ Returns:
33
+ Path: The path to the module.
34
+ """
35
+ try:
36
+ if (src := find_spec(dotted_path)) is None: # pragma: no cover
37
+ msg = f"Couldn't find the path for {dotted_path}"
38
+ raise TypeError(msg)
39
+ except ModuleNotFoundError as e:
40
+ msg = f"Couldn't find the path for {dotted_path}"
41
+ raise TypeError(msg) from e
42
+
43
+ path = Path(str(src.origin))
44
+ return path.parent if path.is_file() else path
45
+
46
+
47
+ def import_string(dotted_path: str) -> Any:
48
+ """Dotted Path Import.
49
+
50
+ Import a dotted module path and return the attribute/class designated by the
51
+ last name in the path. Raise ImportError if the import failed.
52
+
53
+ Args:
54
+ dotted_path: The path of the module to import.
55
+
56
+ Raises:
57
+ ImportError: Could not import the module.
58
+
59
+ Returns:
60
+ object: The imported object.
61
+ """
62
+
63
+ def _is_loaded(module: ModuleType | None) -> bool:
64
+ spec = getattr(module, "__spec__", None)
65
+ initializing = getattr(spec, "_initializing", False)
66
+ return bool(module and spec and not initializing)
67
+
68
+ def _cached_import(module_path: str, class_name: str) -> Any:
69
+ """Import and cache a class from a module.
70
+
71
+ Args:
72
+ module_path: dotted path to module.
73
+ class_name: Class or function name.
74
+
75
+ Returns:
76
+ object: The imported class or function
77
+ """
78
+ # Check whether module is loaded and fully initialized.
79
+ module = sys.modules.get(module_path)
80
+ if not _is_loaded(module):
81
+ module = import_module(module_path)
82
+ return getattr(module, class_name)
83
+
84
+ try:
85
+ module_path, class_name = dotted_path.rsplit(".", 1)
86
+ except ValueError as e:
87
+ msg = "%s doesn't look like a module path"
88
+ raise ImportError(msg, dotted_path) from e
89
+
90
+ try:
91
+ return _cached_import(module_path, class_name)
92
+ except AttributeError as e:
93
+ msg = "Module '%s' does not define a '%s' attribute/class"
94
+ raise ImportError(msg, module_path, class_name) from e