sqlspec 0.14.1__py3-none-any.whl → 0.15.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.

Files changed (158) hide show
  1. sqlspec/__init__.py +50 -25
  2. sqlspec/__main__.py +1 -1
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +256 -120
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +115 -260
  10. sqlspec/adapters/adbc/driver.py +462 -367
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +199 -129
  14. sqlspec/adapters/aiosqlite/driver.py +230 -269
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -168
  18. sqlspec/adapters/asyncmy/driver.py +260 -225
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +82 -181
  22. sqlspec/adapters/asyncpg/driver.py +285 -383
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -258
  26. sqlspec/adapters/bigquery/driver.py +474 -646
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +415 -351
  30. sqlspec/adapters/duckdb/driver.py +343 -413
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -379
  34. sqlspec/adapters/oracledb/driver.py +507 -560
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -254
  38. sqlspec/adapters/psqlpy/driver.py +505 -234
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -403
  42. sqlspec/adapters/psycopg/driver.py +706 -872
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +202 -118
  46. sqlspec/adapters/sqlite/driver.py +264 -303
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder → builder}/_base.py +120 -55
  50. sqlspec/{statement/builder → builder}/_column.py +17 -6
  51. sqlspec/{statement/builder → builder}/_ddl.py +46 -79
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
  53. sqlspec/{statement/builder → builder}/_delete.py +6 -25
  54. sqlspec/{statement/builder → builder}/_insert.py +6 -64
  55. sqlspec/builder/_merge.py +56 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +3 -10
  57. sqlspec/{statement/builder → builder}/_select.py +11 -56
  58. sqlspec/{statement/builder → builder}/_update.py +12 -18
  59. sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
  60. sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
  61. sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +22 -16
  62. sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
  63. sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +3 -5
  64. sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
  65. sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
  66. sqlspec/{statement/builder → builder}/mixins/_select_operations.py +21 -36
  67. sqlspec/{statement/builder → builder}/mixins/_update_operations.py +3 -14
  68. sqlspec/{statement/builder → builder}/mixins/_where_clause.py +52 -79
  69. sqlspec/cli.py +4 -5
  70. sqlspec/config.py +180 -133
  71. sqlspec/core/__init__.py +63 -0
  72. sqlspec/core/cache.py +873 -0
  73. sqlspec/core/compiler.py +396 -0
  74. sqlspec/core/filters.py +828 -0
  75. sqlspec/core/hashing.py +310 -0
  76. sqlspec/core/parameters.py +1209 -0
  77. sqlspec/core/result.py +664 -0
  78. sqlspec/{statement → core}/splitter.py +321 -191
  79. sqlspec/core/statement.py +651 -0
  80. sqlspec/driver/__init__.py +7 -10
  81. sqlspec/driver/_async.py +387 -176
  82. sqlspec/driver/_common.py +527 -289
  83. sqlspec/driver/_sync.py +390 -172
  84. sqlspec/driver/mixins/__init__.py +2 -19
  85. sqlspec/driver/mixins/_result_tools.py +168 -0
  86. sqlspec/driver/mixins/_sql_translator.py +6 -3
  87. sqlspec/exceptions.py +5 -252
  88. sqlspec/extensions/aiosql/adapter.py +93 -96
  89. sqlspec/extensions/litestar/config.py +0 -1
  90. sqlspec/extensions/litestar/handlers.py +15 -26
  91. sqlspec/extensions/litestar/plugin.py +16 -14
  92. sqlspec/extensions/litestar/providers.py +17 -52
  93. sqlspec/loader.py +424 -105
  94. sqlspec/migrations/__init__.py +12 -0
  95. sqlspec/migrations/base.py +92 -68
  96. sqlspec/migrations/commands.py +24 -106
  97. sqlspec/migrations/loaders.py +402 -0
  98. sqlspec/migrations/runner.py +49 -51
  99. sqlspec/migrations/tracker.py +31 -44
  100. sqlspec/migrations/utils.py +64 -24
  101. sqlspec/protocols.py +7 -183
  102. sqlspec/storage/__init__.py +1 -1
  103. sqlspec/storage/backends/base.py +37 -40
  104. sqlspec/storage/backends/fsspec.py +136 -112
  105. sqlspec/storage/backends/obstore.py +138 -160
  106. sqlspec/storage/capabilities.py +5 -4
  107. sqlspec/storage/registry.py +57 -106
  108. sqlspec/typing.py +136 -115
  109. sqlspec/utils/__init__.py +2 -3
  110. sqlspec/utils/correlation.py +0 -3
  111. sqlspec/utils/deprecation.py +6 -6
  112. sqlspec/utils/fixtures.py +6 -6
  113. sqlspec/utils/logging.py +0 -2
  114. sqlspec/utils/module_loader.py +7 -12
  115. sqlspec/utils/singleton.py +0 -1
  116. sqlspec/utils/sync_tools.py +16 -37
  117. sqlspec/utils/text.py +12 -51
  118. sqlspec/utils/type_guards.py +443 -232
  119. {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/METADATA +7 -2
  120. sqlspec-0.15.0.dist-info/RECORD +134 -0
  121. sqlspec/adapters/adbc/transformers.py +0 -108
  122. sqlspec/driver/connection.py +0 -207
  123. sqlspec/driver/mixins/_cache.py +0 -114
  124. sqlspec/driver/mixins/_csv_writer.py +0 -91
  125. sqlspec/driver/mixins/_pipeline.py +0 -508
  126. sqlspec/driver/mixins/_query_tools.py +0 -796
  127. sqlspec/driver/mixins/_result_utils.py +0 -138
  128. sqlspec/driver/mixins/_storage.py +0 -912
  129. sqlspec/driver/mixins/_type_coercion.py +0 -128
  130. sqlspec/driver/parameters.py +0 -138
  131. sqlspec/statement/__init__.py +0 -21
  132. sqlspec/statement/builder/_merge.py +0 -95
  133. sqlspec/statement/cache.py +0 -50
  134. sqlspec/statement/filters.py +0 -625
  135. sqlspec/statement/parameters.py +0 -956
  136. sqlspec/statement/pipelines/__init__.py +0 -210
  137. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  138. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  139. sqlspec/statement/pipelines/context.py +0 -109
  140. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  141. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  142. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  143. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  144. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  145. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  146. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  147. sqlspec/statement/pipelines/validators/_performance.py +0 -714
  148. sqlspec/statement/pipelines/validators/_security.py +0 -967
  149. sqlspec/statement/result.py +0 -435
  150. sqlspec/statement/sql.py +0 -1774
  151. sqlspec/utils/cached_property.py +0 -25
  152. sqlspec/utils/statement_hashing.py +0 -203
  153. sqlspec-0.14.1.dist-info/RECORD +0 -145
  154. /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
  155. {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/WHEEL +0 -0
  156. {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/entry_points.txt +0 -0
  157. {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/licenses/LICENSE +0 -0
  158. {sqlspec-0.14.1.dist-info → sqlspec-0.15.0.dist-info}/licenses/NOTICE +0 -0
@@ -23,12 +23,12 @@ def warn_deprecation(
23
23
  info: Optional[str] = None,
24
24
  pending: bool = False,
25
25
  ) -> None:
26
- """Warn about a call to a (soon to be) deprecated function.
26
+ """Warn about a call to a deprecated function.
27
27
 
28
28
  Args:
29
- version: Advanced Alchemy version where the deprecation will occur
29
+ version: SQLSpec version where the deprecation will occur
30
30
  deprecated_name: Name of the deprecated function
31
- removal_in: Advanced Alchemy version where the deprecated function will be removed
31
+ removal_in: SQLSpec version where the deprecated function will be removed
32
32
  alternative: Name of a function that should be used instead
33
33
  info: Additional information
34
34
  pending: Use :class:`warnings.PendingDeprecationWarning` instead of :class:`warnings.DeprecationWarning`
@@ -72,11 +72,11 @@ def deprecated(
72
72
  pending: bool = False,
73
73
  kind: Optional[Literal["function", "method", "classmethod", "property"]] = None,
74
74
  ) -> Callable[[Callable[P, T]], Callable[P, T]]:
75
- """Create a decorator wrapping a function, method or property with a warning call about a (pending) deprecation.
75
+ """Create a decorator wrapping a function, method or property with a deprecation warning.
76
76
 
77
77
  Args:
78
- version: Litestar version where the deprecation will occur
79
- removal_in: Litestar version where the deprecated function will be removed
78
+ version: SQLSpec version where the deprecation will occur
79
+ removal_in: SQLSpec version where the deprecated function will be removed
80
80
  alternative: Name of a function that should be used instead
81
81
  info: Additional information
82
82
  pending: Use :class:`warnings.PendingDeprecationWarning` instead of :class:`warnings.DeprecationWarning`
sqlspec/utils/fixtures.py CHANGED
@@ -8,17 +8,17 @@ __all__ = ("open_fixture", "open_fixture_async")
8
8
 
9
9
 
10
10
  def open_fixture(fixtures_path: Any, fixture_name: str) -> Any:
11
- """Loads JSON file with the specified fixture name
11
+ """Load and parse a JSON fixture file.
12
12
 
13
13
  Args:
14
14
  fixtures_path: The path to look for fixtures (pathlib.Path or anyio.Path)
15
- fixture_name (str): The fixture name to load.
15
+ fixture_name: The fixture name to load.
16
16
 
17
17
  Raises:
18
18
  FileNotFoundError: Fixtures not found.
19
19
 
20
20
  Returns:
21
- Any: The parsed JSON data
21
+ The parsed JSON data
22
22
  """
23
23
 
24
24
  fixture = Path(fixtures_path / f"{fixture_name}.json")
@@ -31,18 +31,18 @@ def open_fixture(fixtures_path: Any, fixture_name: str) -> Any:
31
31
 
32
32
 
33
33
  async def open_fixture_async(fixtures_path: Any, fixture_name: str) -> Any:
34
- """Loads JSON file with the specified fixture name
34
+ """Load and parse a JSON fixture file asynchronously.
35
35
 
36
36
  Args:
37
37
  fixtures_path: The path to look for fixtures (pathlib.Path or anyio.Path)
38
- fixture_name (str): The fixture name to load.
38
+ fixture_name: The fixture name to load.
39
39
 
40
40
  Raises:
41
41
  FileNotFoundError: Fixtures not found.
42
42
  MissingDependencyError: The `anyio` library is required to use this function.
43
43
 
44
44
  Returns:
45
- Any: The parsed JSON data
45
+ The parsed JSON data
46
46
  """
47
47
  try:
48
48
  from anyio import Path as AsyncPath
sqlspec/utils/logging.py CHANGED
@@ -18,7 +18,6 @@ if TYPE_CHECKING:
18
18
 
19
19
  __all__ = ("StructuredFormatter", "correlation_id_var", "get_correlation_id", "get_logger", "set_correlation_id")
20
20
 
21
- # Context variable for correlation ID tracking
22
21
  correlation_id_var: ContextVar[str | None] = ContextVar("correlation_id", default=None)
23
22
 
24
23
 
@@ -52,7 +51,6 @@ class StructuredFormatter(logging.Formatter):
52
51
  Returns:
53
52
  JSON formatted log entry
54
53
  """
55
- # Base log entry
56
54
  log_entry = {
57
55
  "timestamp": self.formatTime(record, self.datefmt),
58
56
  "level": record.levelname,
@@ -9,19 +9,16 @@ __all__ = ("import_string", "module_to_os_path")
9
9
 
10
10
 
11
11
  def module_to_os_path(dotted_path: str = "app") -> "Path":
12
- """Find Module to OS Path.
13
-
14
- Return a path to the base directory of the project or the module
15
- specified by `dotted_path`.
12
+ """Convert a module dotted path to filesystem path.
16
13
 
17
14
  Args:
18
- dotted_path: The path to the module. Defaults to "app".
15
+ dotted_path: The path to the module.
19
16
 
20
17
  Raises:
21
18
  TypeError: The module could not be found.
22
19
 
23
20
  Returns:
24
- Path: The path to the module.
21
+ The path to the module.
25
22
  """
26
23
  try:
27
24
  if (src := find_spec(dotted_path)) is None: # pragma: no cover
@@ -36,16 +33,13 @@ def module_to_os_path(dotted_path: str = "app") -> "Path":
36
33
 
37
34
 
38
35
  def import_string(dotted_path: str) -> "Any":
39
- """Dotted Path Import.
40
-
41
- Import a dotted module path and return the attribute/class designated by the
42
- last name in the path. Raise ImportError if the import failed.
36
+ """Import a module or attribute from a dotted path string.
43
37
 
44
38
  Args:
45
39
  dotted_path: The path of the module to import.
46
40
 
47
41
  Returns:
48
- object: The imported object.
42
+ The imported object.
49
43
  """
50
44
 
51
45
  def _raise_import_error(msg: str, exc: "Optional[Exception]" = None) -> None:
@@ -57,7 +51,7 @@ def import_string(dotted_path: str) -> "Any":
57
51
  try:
58
52
  parts = dotted_path.split(".")
59
53
  module = None
60
- i = len(parts) # Initialize to full length
54
+ i = len(parts)
61
55
 
62
56
  for i in range(len(parts), 0, -1):
63
57
  module_path = ".".join(parts[:i])
@@ -83,6 +77,7 @@ def import_string(dotted_path: str) -> "Any":
83
77
  return obj
84
78
  if not hasattr(parent_module, attr):
85
79
  _raise_import_error(f"Module '{parent_module_path}' has no attribute '{attr}' in '{dotted_path}'")
80
+
86
81
  for attr in attrs:
87
82
  if not hasattr(obj, attr):
88
83
  _raise_import_error(
@@ -10,7 +10,6 @@ _T = TypeVar("_T")
10
10
  class SingletonMeta(type):
11
11
  """Metaclass for singleton pattern."""
12
12
 
13
- # We store instances keyed by the class type
14
13
  _instances: dict[type, object] = {}
15
14
  _lock = threading.Lock()
16
15
 
@@ -61,11 +61,10 @@ def run_(async_function: "Callable[ParamSpecT, Coroutine[Any, Any, ReturnT]]") -
61
61
  """Convert an async function to a blocking function using asyncio.run().
62
62
 
63
63
  Args:
64
- async_function (Callable): The async function to convert.
64
+ async_function: The async function to convert.
65
65
 
66
66
  Returns:
67
- Callable: A blocking function that runs the async function.
68
-
67
+ A blocking function that runs the async function.
69
68
  """
70
69
 
71
70
  @functools.wraps(async_function)
@@ -77,7 +76,6 @@ def run_(async_function: "Callable[ParamSpecT, Coroutine[Any, Any, ReturnT]]") -
77
76
  loop = None
78
77
 
79
78
  if loop is not None:
80
- # Running in an existing event loop
81
79
  return asyncio.run(partial_f())
82
80
  if uvloop and sys.platform != "win32":
83
81
  uvloop.install() # pyright: ignore[reportUnknownMemberType]
@@ -92,12 +90,12 @@ def await_(
92
90
  """Convert an async function to a blocking one, running in the main async loop.
93
91
 
94
92
  Args:
95
- async_function (Callable): The async function to convert.
96
- raise_sync_error (bool, optional): If False, runs in a new event loop if no loop is present.
97
- If True (default), raises RuntimeError if no loop is running.
93
+ async_function: The async function to convert.
94
+ raise_sync_error: If False, runs in a new event loop if no loop is present.
95
+ If True (default), raises RuntimeError if no loop is running.
98
96
 
99
97
  Returns:
100
- Callable: A blocking function that runs the async function.
98
+ A blocking function that runs the async function.
101
99
  """
102
100
 
103
101
  @functools.wraps(async_function)
@@ -111,27 +109,17 @@ def await_(
111
109
  raise RuntimeError(msg) from None
112
110
  return asyncio.run(partial_f())
113
111
  else:
114
- # Running in an existing event loop.
115
112
  if loop.is_running():
116
113
  try:
117
114
  current_task = asyncio.current_task(loop=loop)
118
115
  except RuntimeError:
119
- # Not running inside a task managed by this loop
120
116
  current_task = None
121
117
 
122
118
  if current_task is not None:
123
- # Called from within the event loop's execution context (a task).
124
- # Blocking here would deadlock the loop.
125
119
  msg = "await_ cannot be called from within an async task running on the same event loop. Use 'await' instead."
126
120
  raise RuntimeError(msg)
127
- # Called from a different thread than the loop's thread.
128
- # It's safe to block this thread and wait for the loop.
129
121
  future = asyncio.run_coroutine_threadsafe(partial_f(), loop)
130
- # This blocks the *calling* thread, not the loop thread.
131
122
  return future.result()
132
- # This case should ideally not happen if get_running_loop() succeeded
133
- # but the loop isn't running, but handle defensively.
134
- # loop is not running
135
123
  if raise_sync_error:
136
124
  msg = "Cannot run async function"
137
125
  raise RuntimeError(msg)
@@ -146,12 +134,11 @@ def async_(
146
134
  """Convert a blocking function to an async one using asyncio.to_thread().
147
135
 
148
136
  Args:
149
- function (Callable): The blocking function to convert.
150
- cancellable (bool, optional): Allow cancellation of the operation.
151
- limiter (CapacityLimiter, optional): Limit the total number of threads.
137
+ function: The blocking function to convert.
138
+ limiter: Limit the total number of threads.
152
139
 
153
140
  Returns:
154
- Callable: An async function that runs the original function in a thread.
141
+ An async function that runs the original function in a thread.
155
142
  """
156
143
 
157
144
  @functools.wraps(function)
@@ -170,10 +157,10 @@ def ensure_async_(
170
157
  """Convert a function to an async one if it is not already.
171
158
 
172
159
  Args:
173
- function (Callable): The function to convert.
160
+ function: The function to convert.
174
161
 
175
162
  Returns:
176
- Callable: An async function that runs the original function.
163
+ An async function that runs the original function.
177
164
  """
178
165
  if inspect.iscoroutinefunction(function):
179
166
  return function
@@ -210,10 +197,10 @@ def with_ensure_async_(
210
197
  """Convert a context manager to an async one if it is not already.
211
198
 
212
199
  Args:
213
- obj (AbstractContextManager[T] or AbstractAsyncContextManager[T]): The context manager to convert.
200
+ obj: The context manager to convert.
214
201
 
215
202
  Returns:
216
- AbstractAsyncContextManager[T]: An async context manager that runs the original context manager.
203
+ An async context manager that runs the original context manager.
217
204
  """
218
205
 
219
206
  if isinstance(obj, AbstractContextManager):
@@ -222,30 +209,22 @@ def with_ensure_async_(
222
209
 
223
210
 
224
211
  class NoValue:
225
- """A fake "Empty class"""
212
+ """Sentinel class for missing values."""
226
213
 
227
214
 
228
215
  async def get_next(iterable: Any, default: Any = NoValue, *args: Any) -> Any: # pragma: no cover
229
216
  """Return the next item from an async iterator.
230
217
 
231
- In Python <3.10, `anext` is not available. This function is a drop-in replacement.
232
-
233
- This function will return the next value form an async iterable. If the
234
- iterable is empty the StopAsyncIteration will be propagated. However, if
235
- a default value is given as a second argument the exception is silenced and
236
- the default value is returned instead.
237
-
238
218
  Args:
239
219
  iterable: An async iterable.
240
220
  default: An optional default value to return if the iterable is empty.
241
221
  *args: The remaining args
242
- Return:
222
+
223
+ Returns:
243
224
  The next value of the iterable.
244
225
 
245
226
  Raises:
246
227
  StopAsyncIteration: The iterable given is not async.
247
-
248
-
249
228
  """
250
229
  has_default = bool(not isinstance(default, NoValue))
251
230
  try:
sqlspec/utils/text.py CHANGED
@@ -5,35 +5,29 @@ import unicodedata
5
5
  from functools import lru_cache
6
6
  from typing import Optional
7
7
 
8
- # Compiled regex for slugify
9
8
  _SLUGIFY_REMOVE_NON_ALPHANUMERIC = re.compile(r"[^\w]+", re.UNICODE)
10
9
  _SLUGIFY_HYPHEN_COLLAPSE = re.compile(r"-+")
11
10
 
12
- # Compiled regex for snake_case
13
- # Insert underscore between lowercase/digit and uppercase letter
14
11
  _SNAKE_CASE_LOWER_OR_DIGIT_TO_UPPER = re.compile(r"(?<=[a-z0-9])(?=[A-Z])", re.UNICODE)
15
- # Insert underscore between uppercase letter and uppercase followed by lowercase
16
12
  _SNAKE_CASE_UPPER_TO_UPPER_LOWER = re.compile(r"(?<=[A-Z])(?=[A-Z][a-z])", re.UNICODE)
17
13
  _SNAKE_CASE_HYPHEN_SPACE = re.compile(r"[.\s@-]+", re.UNICODE)
18
- # Collapse multiple underscores
14
+ _SNAKE_CASE_REMOVE_NON_WORD = re.compile(r"[^\w]+", re.UNICODE)
19
15
  _SNAKE_CASE_MULTIPLE_UNDERSCORES = re.compile(r"__+", re.UNICODE)
20
16
 
21
17
  __all__ = ("camelize", "check_email", "slugify", "snake_case")
22
18
 
23
19
 
24
20
  def check_email(email: str) -> str:
25
- """Validate an email.
26
-
27
- Very simple email validation.
21
+ """Validate an email address.
28
22
 
29
23
  Args:
30
- email (str): The email to validate.
24
+ email: The email to validate.
31
25
 
32
26
  Raises:
33
27
  ValueError: If the email is invalid.
34
28
 
35
29
  Returns:
36
- str: The validated email.
30
+ The validated email.
37
31
  """
38
32
  if "@" not in email:
39
33
  msg = "Invalid email!"
@@ -42,21 +36,15 @@ def check_email(email: str) -> str:
42
36
 
43
37
 
44
38
  def slugify(value: str, allow_unicode: bool = False, separator: Optional[str] = None) -> str:
45
- """Slugify.
46
-
47
- Convert to ASCII if ``allow_unicode`` is ``False``. Convert spaces or repeated
48
- dashes to single dashes. Remove characters that aren't alphanumerics,
49
- underscores, or hyphens. Convert to lowercase. Also strip leading and
50
- trailing whitespace, dashes, and underscores.
39
+ """Convert a string to a URL-friendly slug.
51
40
 
52
41
  Args:
53
- value (str): the string to slugify
54
- allow_unicode (bool, optional): allow unicode characters in slug. Defaults to False.
55
- separator (str, optional): by default a `-` is used to delimit word boundaries.
56
- Set this to configure something different.
42
+ value: The string to slugify
43
+ allow_unicode: Allow unicode characters in slug.
44
+ separator: Separator character for word boundaries. Defaults to "-".
57
45
 
58
46
  Returns:
59
- str: a slugified string of the value parameter
47
+ A slugified string.
60
48
  """
61
49
  if allow_unicode:
62
50
  value = unicodedata.normalize("NFKC", value)
@@ -67,9 +55,7 @@ def slugify(value: str, allow_unicode: bool = False, separator: Optional[str] =
67
55
  if not sep:
68
56
  return _SLUGIFY_REMOVE_NON_ALPHANUMERIC.sub("", value)
69
57
  value = _SLUGIFY_REMOVE_NON_ALPHANUMERIC.sub(sep, value)
70
- # For dynamic separators, we need to use re.sub with escaped separator
71
58
  if sep == "-":
72
- # Use pre-compiled regex for common case
73
59
  value = value.strip("-")
74
60
  return _SLUGIFY_HYPHEN_COLLAPSE.sub("-", value)
75
61
  value = re.sub(rf"^{re.escape(sep)}+|{re.escape(sep)}+$", "", value)
@@ -81,10 +67,10 @@ def camelize(string: str) -> str:
81
67
  """Convert a string to camel case.
82
68
 
83
69
  Args:
84
- string (str): The string to convert.
70
+ string: The string to convert.
85
71
 
86
72
  Returns:
87
- str: The converted string.
73
+ The converted string.
88
74
  """
89
75
  return "".join(word if index == 0 else word.capitalize() for index, word in enumerate(string.split("_")))
90
76
 
@@ -93,11 +79,6 @@ def camelize(string: str) -> str:
93
79
  def snake_case(string: str) -> str:
94
80
  """Convert a string to snake_case.
95
81
 
96
- Handles CamelCase, PascalCase, strings with spaces, hyphens, or dots
97
- as separators, and ensures single underscores. It also correctly
98
- handles acronyms (e.g., "HTTPRequest" becomes "http_request").
99
- Handles Unicode letters and numbers.
100
-
101
82
  Args:
102
83
  string: The string to convert.
103
84
 
@@ -106,30 +87,10 @@ def snake_case(string: str) -> str:
106
87
  """
107
88
  if not string:
108
89
  return ""
109
- # 1. Replace hyphens and spaces with underscores
110
90
  s = _SNAKE_CASE_HYPHEN_SPACE.sub("_", string)
111
-
112
- # 2. Remove all non-alphanumeric characters except underscores
113
- # TODO: move to a compiled regex at the top of the file
114
- s = re.sub(r"[^\w]+", "", s, flags=re.UNICODE)
115
-
116
- # 3. Insert an underscore between a lowercase/digit and an uppercase letter.
117
- # e.g., "helloWorld" -> "hello_World"
118
- # e.g., "Python3IsGreat" -> "Python3_IsGreat"
119
- # Uses a positive lookbehind `(?<=[...])` and a positive lookahead `(?=[...])`
91
+ s = _SNAKE_CASE_REMOVE_NON_WORD.sub("", s)
120
92
  s = _SNAKE_CASE_LOWER_OR_DIGIT_TO_UPPER.sub("_", s)
121
-
122
- # 4. Insert an underscore between an uppercase letter and another
123
- # uppercase letter followed by a lowercase letter.
124
- # e.g., "HTTPRequest" -> "HTTP_Request"
125
- # This handles acronyms gracefully.
126
93
  s = _SNAKE_CASE_UPPER_TO_UPPER_LOWER.sub("_", s)
127
-
128
- # 5. Convert the entire string to lowercase.
129
94
  s = s.lower()
130
-
131
- # 6. Remove any leading or trailing underscores that might have been created.
132
95
  s = s.strip("_")
133
-
134
- # 7. Collapse multiple consecutive underscores into a single one.
135
96
  return _SNAKE_CASE_MULTIPLE_UNDERSCORES.sub("_", s)