dycw-utilities 0.150.7__py3-none-any.whl → 0.150.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.150.7
3
+ Version: 0.150.9
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=A1YeWIQQEjqfqiCFF31G03YhEM20RzT8IBH3dcnt8d4,60
1
+ utilities/__init__.py,sha256=GsG3Q0h41dPHASCsd70D14gPFgnAp7k4Bz18oGu00SI,60
2
2
  utilities/altair.py,sha256=92E2lCdyHY4Zb-vCw6rEJIsWdKipuu-Tu2ab1ufUfAk,9079
3
3
  utilities/asyncio.py,sha256=2m2a2C-Qgc6OHTTHL332-t66A7xDITt_SORT7a1DJWo,16792
4
4
  utilities/atomicwrites.py,sha256=xcOWenTBRS0oat3kg7Sqe51AohNThMQ2ixPL7QCG8hw,5795
@@ -23,7 +23,7 @@ utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
23
23
  utilities/gzip.py,sha256=fkGP3KdsBfXlstodT4wtlp-PwNyUsogpbDCVVVGdsm4,781
24
24
  utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
25
25
  utilities/http.py,sha256=TsavEfHlRtlLaeV21Z6KZh0qbPw-kvD1zsQdZ7Kep5Q,977
26
- utilities/hypothesis.py,sha256=i-TUh9gCgoPMfFYueVwoaUIHtCbZn99tXUkrPvxZxG4,38069
26
+ utilities/hypothesis.py,sha256=_DKCFAHgE4NRE1asv-asYVvs5cqqivxTNrKHPeq2RjE,39282
27
27
  utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
28
28
  utilities/inflect.py,sha256=UWOsMRJUJXqcR3rgrQmb35mat-Theu5xo-M9E33eTIE,586
29
29
  utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
@@ -49,7 +49,7 @@ utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
49
49
  utilities/platform.py,sha256=Ue9LSxYvg9yUXGKuz5aZoy_qkUEXde-v6B09exgSctU,2813
50
50
  utilities/polars.py,sha256=BgiDryAVOapi41ddfJqN0wYh_sDj8BNEYtPB36LaHdo,71824
51
51
  utilities/polars_ols.py,sha256=Uc9V5kvlWZ5cU93lKZ-cfAKdVFFw81tqwLW9PxtUvMs,5618
52
- utilities/postgres.py,sha256=L1cunDLu8nK5fCkY9fTgSTHjT1_B8Y0iUJEjPC7Ar3A,13849
52
+ utilities/postgres.py,sha256=48km6Mb6hRUb1-0FWg7FKEfrW75z8-7AY1jahtdu_KY,12406
53
53
  utilities/pottery.py,sha256=u0uvyGgYyujxftEMlsv6ppYTKQoVVjHt5jnVxxYz9s4,6596
54
54
  utilities/pqdm.py,sha256=BTsYPtbKQWwX-iXF4qCkfPG7DPxIB54J989n83bXrIo,3092
55
55
  utilities/psutil.py,sha256=KUlu4lrUw9Zg1V7ZGetpWpGb9DB8l_SSDWGbANFNCPU,2104
@@ -66,12 +66,12 @@ utilities/sentinel.py,sha256=3jIwgpMekWgDAxPDA_hXMP2St43cPhciKN3LWiZ7kv0,1248
66
66
  utilities/shelve.py,sha256=4OzjQI6kGuUbJciqf535rwnao-_IBv66gsT6tRGiUt0,759
67
67
  utilities/slack_sdk.py,sha256=ppFBvKgfg5IRWiIoKPtpTyzBtBF4XmwEvU3I5wLJikM,2140
68
68
  utilities/socket.py,sha256=K77vfREvzoVTrpYKo6MZakol0EYu2q1sWJnnZqL0So0,118
69
- utilities/sqlalchemy.py,sha256=5TPqPvJUCuxIeiMD7IKTptYg11UXNy7ThGw81reCIRU,37290
69
+ utilities/sqlalchemy.py,sha256=u2ZVvr_4Yrqsur6FlLHUuW0mPaoNrHvaJ2wj7o_WG3A,39264
70
70
  utilities/sqlalchemy_polars.py,sha256=18AoEbeNJUKF3-5hroNy9J5LQwS_QJAXbMfKc9sChtk,14250
71
71
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
72
72
  utilities/string.py,sha256=MB0X6UPTUc06JdAdj-PctZ238IXeCjE5dAJibNw6ZrU,587
73
73
  utilities/tempfile.py,sha256=HxB2BF28CcecDJLQ3Bx2Ej-Pb6RJc6W9ngSpB9CnP4k,2018
74
- utilities/text.py,sha256=ymBFlP_cA8OgNnZRVNs7FAh7OG8HxE6YkiLEMZv5g_A,11297
74
+ utilities/text.py,sha256=bupgC6ILTjmcJKSUGloStzmWuj2Ke0knvVKE2mWLwAM,11619
75
75
  utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
76
76
  utilities/timer.py,sha256=oXfTii6ymu57niP0BDGZjFD55LEHi2a19kqZKiTgaFQ,2588
77
77
  utilities/traceback.py,sha256=zofhzIedpUHrzDNiRJDVzm_wuu_tlTQvVqK4quxVlgM,9151
@@ -89,8 +89,8 @@ utilities/zoneinfo.py,sha256=oEH-nL3t4h9uawyZqWDtNtDAl6M-CLpLYGI_nI6DulM,1971
89
89
  utilities/pytest_plugins/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
90
90
  utilities/pytest_plugins/pytest_randomly.py,sha256=NXzCcGKbpgYouz5yehKb4jmxmi2SexKKpgF4M65bi10,414
91
91
  utilities/pytest_plugins/pytest_regressions.py,sha256=Iwhfv_OJH7UCPZCfoh7ugZ2Xjqjil-BBBsOb8sDwiGI,1471
92
- dycw_utilities-0.150.7.dist-info/METADATA,sha256=RtXoOrng-vQlgoP2v8k9pfCYWGKzLTAAXhfQN7UADA4,1696
93
- dycw_utilities-0.150.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
- dycw_utilities-0.150.7.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
95
- dycw_utilities-0.150.7.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
96
- dycw_utilities-0.150.7.dist-info/RECORD,,
92
+ dycw_utilities-0.150.9.dist-info/METADATA,sha256=h1ki8nwAV6ET-w-Z9jFMsFrXnEezGVzts3XVeQ9kJps,1696
93
+ dycw_utilities-0.150.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
+ dycw_utilities-0.150.9.dist-info/entry_points.txt,sha256=BOD_SoDxwsfJYOLxhrSXhHP_T7iw-HXI9f2WVkzYxvQ,135
95
+ dycw_utilities-0.150.9.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
96
+ dycw_utilities-0.150.9.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.150.7"
3
+ __version__ = "0.150.9"
utilities/hypothesis.py CHANGED
@@ -98,6 +98,7 @@ if TYPE_CHECKING:
98
98
  from hypothesis.database import ExampleDatabase
99
99
  from libcst import Import, ImportFrom
100
100
  from numpy.random import RandomState
101
+ from sqlalchemy import URL
101
102
 
102
103
  from utilities.numpy import NDArrayB, NDArrayF, NDArrayI, NDArrayO
103
104
  from utilities.types import Number, TimeZoneLike
@@ -1258,6 +1259,41 @@ def uint64s(
1258
1259
  ##
1259
1260
 
1260
1261
 
1262
+ @composite
1263
+ def urls(
1264
+ draw: DrawFn,
1265
+ /,
1266
+ *,
1267
+ all_: MaybeSearchStrategy[bool] = False,
1268
+ username: MaybeSearchStrategy[bool] = False,
1269
+ password: MaybeSearchStrategy[bool] = False,
1270
+ host: MaybeSearchStrategy[bool] = False,
1271
+ port: MaybeSearchStrategy[bool] = False,
1272
+ database: MaybeSearchStrategy[bool] = False,
1273
+ ) -> URL:
1274
+ from sqlalchemy import URL
1275
+
1276
+ have_all, have_username, have_password, have_host, have_port, have_database = [
1277
+ draw2(draw, b) for b in [all_, username, password, host, port, database]
1278
+ ]
1279
+ username_use = draw(text_ascii(min_size=1)) if have_all or have_username else None
1280
+ password_use = draw(text_ascii(min_size=1)) if have_all or have_password else None
1281
+ host_use = draw(text_ascii(min_size=1)) if have_all or have_host else None
1282
+ port_use = draw(integers(min_value=1)) if have_all or have_port else None
1283
+ database_use = draw(text_ascii(min_size=1)) if have_all or have_database else None
1284
+ return URL.create(
1285
+ drivername="sqlite",
1286
+ username=username_use,
1287
+ password=password_use,
1288
+ host=host_use,
1289
+ port=port_use,
1290
+ database=database_use,
1291
+ )
1292
+
1293
+
1294
+ ##
1295
+
1296
+
1261
1297
  @composite
1262
1298
  def versions(draw: DrawFn, /, *, suffix: MaybeSearchStrategy[bool] = False) -> Version:
1263
1299
  """Strategy for generating versions."""
@@ -1392,6 +1428,7 @@ __all__ = [
1392
1428
  "triples",
1393
1429
  "uint32s",
1394
1430
  "uint64s",
1431
+ "urls",
1395
1432
  "versions",
1396
1433
  "year_months",
1397
1434
  "zoned_datetimes",
utilities/postgres.py CHANGED
@@ -13,7 +13,7 @@ from utilities.iterables import always_iterable
13
13
  from utilities.logging import get_logger
14
14
  from utilities.os import temp_environ
15
15
  from utilities.pathlib import ensure_suffix
16
- from utilities.sqlalchemy import get_table_name
16
+ from utilities.sqlalchemy import extract_url, get_table_name
17
17
  from utilities.timer import Timer
18
18
  from utilities.types import PathLike
19
19
 
@@ -124,12 +124,11 @@ def _build_pg_dump(
124
124
  role: str | None = None,
125
125
  docker: str | None = None,
126
126
  ) -> str:
127
- database, host, port = _extract_url(url)
127
+ extracted = extract_url(url)
128
128
  path = _path_pg_dump(path, format_=format_)
129
129
  parts: list[str] = [
130
130
  "pg_dump",
131
131
  # general options
132
- f"--dbname={database}",
133
132
  f"--file={str(path)!r}",
134
133
  f"--format={format_}",
135
134
  "--verbose",
@@ -139,8 +138,10 @@ def _build_pg_dump(
139
138
  "--no-owner",
140
139
  "--no-privileges",
141
140
  # connection options
142
- f"--host={host}",
143
- f"--port={port}",
141
+ f"--dbname={extracted.database}",
142
+ f"--host={extracted.host}",
143
+ f"--port={extracted.port}",
144
+ f"--username={extracted.username}",
144
145
  "--no-password",
145
146
  ]
146
147
  if (format_ == "directory") and (jobs is not None):
@@ -167,8 +168,6 @@ def _build_pg_dump(
167
168
  parts.append("--inserts")
168
169
  if on_conflict_do_nothing:
169
170
  parts.append("--on-conflict-do-nothing")
170
- if url.username is not None:
171
- parts.append(f"--username={url.username}")
172
171
  if role is not None:
173
172
  parts.append(f"--role={role}")
174
173
  if docker is not None:
@@ -203,7 +202,6 @@ async def restore(
203
202
  /,
204
203
  *,
205
204
  psql: bool = False,
206
- database: str | None = None,
207
205
  data_only: bool = False,
208
206
  clean: bool = False,
209
207
  create: bool = False,
@@ -221,7 +219,6 @@ async def restore(
221
219
  url,
222
220
  path,
223
221
  psql=psql,
224
- database=database,
225
222
  data_only=data_only,
226
223
  clean=clean,
227
224
  create=create,
@@ -270,7 +267,6 @@ def _build_pg_restore_or_psql(
270
267
  /,
271
268
  *,
272
269
  psql: bool = False,
273
- database: str | None = None,
274
270
  data_only: bool = False,
275
271
  clean: bool = False,
276
272
  create: bool = False,
@@ -283,11 +279,10 @@ def _build_pg_restore_or_psql(
283
279
  ) -> str:
284
280
  path = Path(path)
285
281
  if (path.suffix == ".sql") or psql:
286
- return _build_psql(url, path, database=database, docker=docker)
282
+ return _build_psql(url, path, docker=docker)
287
283
  return _build_pg_restore(
288
284
  url,
289
285
  path,
290
- database=database,
291
286
  data_only=data_only,
292
287
  clean=clean,
293
288
  create=create,
@@ -305,7 +300,6 @@ def _build_pg_restore(
305
300
  path: PathLike,
306
301
  /,
307
302
  *,
308
- database: str | None = None,
309
303
  data_only: bool = False,
310
304
  clean: bool = False,
311
305
  create: bool = False,
@@ -317,12 +311,10 @@ def _build_pg_restore(
317
311
  docker: str | None = None,
318
312
  ) -> str:
319
313
  """Run `pg_restore`."""
320
- url_database, host, port = _extract_url(url)
321
- database_use = url_database if database is None else database
314
+ extracted = extract_url(url)
322
315
  parts: list[str] = [
323
316
  "pg_restore",
324
317
  # general options
325
- f"--dbname={database_use}",
326
318
  "--verbose",
327
319
  # restore options
328
320
  *_resolve_data_only_and_clean(data_only=data_only, clean=clean),
@@ -330,8 +322,10 @@ def _build_pg_restore(
330
322
  "--no-owner",
331
323
  "--no-privileges",
332
324
  # connection options
333
- f"--host={host}",
334
- f"--port={port}",
325
+ f"--host={extracted.host}",
326
+ f"--port={extracted.port}",
327
+ f"--username={extracted.username}",
328
+ f"--dbname={extracted.database}",
335
329
  "--no-password",
336
330
  ]
337
331
  if create:
@@ -344,8 +338,6 @@ def _build_pg_restore(
344
338
  parts.extend([f"--exclude-schema={s}" for s in always_iterable(schemas_exc)])
345
339
  if tables is not None:
346
340
  parts.extend([f"--table={_get_table_name(t)}" for t in always_iterable(tables)])
347
- if url.username is not None:
348
- parts.append(f"--username={url.username}")
349
341
  if role is not None:
350
342
  parts.append(f"--role={role}")
351
343
  if docker is not None:
@@ -354,29 +346,20 @@ def _build_pg_restore(
354
346
  return " ".join(parts)
355
347
 
356
348
 
357
- def _build_psql(
358
- url: URL,
359
- path: PathLike,
360
- /,
361
- *,
362
- database: str | None = None,
363
- docker: str | None = None,
364
- ) -> str:
349
+ def _build_psql(url: URL, path: PathLike, /, *, docker: str | None = None) -> str:
365
350
  """Run `psql`."""
366
- url_database, host, port = _extract_url(url)
367
- database_use = url_database if database is None else database
351
+ extracted = extract_url(url)
368
352
  parts: list[str] = [
369
353
  "psql",
370
354
  # general options
371
- f"--dbname={database_use}",
355
+ f"--dbname={extracted.database}",
372
356
  f"--file={str(path)!r}",
373
357
  # connection options
374
- f"--host={host}",
375
- f"--port={port}",
358
+ f"--host={extracted.host}",
359
+ f"--port={extracted.port}",
360
+ f"--username={extracted.username}",
376
361
  "--no-password",
377
362
  ]
378
- if url.username is not None:
379
- parts.append(f"--username={url.username}")
380
363
  if docker is not None:
381
364
  parts = _wrap_docker(parts, docker)
382
365
  return " ".join(parts)
@@ -385,42 +368,6 @@ def _build_psql(
385
368
  ##
386
369
 
387
370
 
388
- def _extract_url(url: URL, /) -> tuple[str, str, int]:
389
- if url.database is None:
390
- raise _ExtractURLDatabaseError(url=url)
391
- if url.host is None:
392
- raise _ExtractURLHostError(url=url)
393
- if url.port is None:
394
- raise _ExtractURLPortError(url=url)
395
- return url.database, url.host, url.port
396
-
397
-
398
- @dataclass(kw_only=True, slots=True)
399
- class ExtractURLError(Exception):
400
- url: URL
401
-
402
-
403
- @dataclass(kw_only=True, slots=True)
404
- class _ExtractURLDatabaseError(ExtractURLError):
405
- @override
406
- def __str__(self) -> str:
407
- return f"Expected URL to contain a 'database'; got {self.url}"
408
-
409
-
410
- @dataclass(kw_only=True, slots=True)
411
- class _ExtractURLHostError(ExtractURLError):
412
- @override
413
- def __str__(self) -> str:
414
- return f"Expected URL to contain a 'host'; got {self.url}"
415
-
416
-
417
- @dataclass(kw_only=True, slots=True)
418
- class _ExtractURLPortError(ExtractURLError):
419
- @override
420
- def __str__(self) -> str:
421
- return f"Expected URL to contain a 'port'; got {self.url}"
422
-
423
-
424
371
  def _get_table_name(obj: TableOrORMInstOrClass | str, /) -> str:
425
372
  match obj:
426
373
  case Table() | DeclarativeBase() | type() as table_or_orm:
@@ -458,4 +405,4 @@ def _wrap_docker(parts: list[str], container: str, /) -> list[str]:
458
405
  return ["docker", "exec", "-it", container, *parts]
459
406
 
460
407
 
461
- __all__ = ["ExtractURLError", "pg_dump", "restore"]
408
+ __all__ = ["pg_dump", "restore"]
utilities/sqlalchemy.py CHANGED
@@ -96,7 +96,7 @@ from utilities.iterables import (
96
96
  one,
97
97
  )
98
98
  from utilities.reprlib import get_repr
99
- from utilities.text import snake_case
99
+ from utilities.text import secret_str, snake_case
100
100
  from utilities.types import MaybeIterable, MaybeType, StrMapping, TupleOrStrMapping
101
101
 
102
102
  if TYPE_CHECKING:
@@ -378,6 +378,79 @@ def enum_values(enum: type[StrEnum], /) -> list[str]:
378
378
  ##
379
379
 
380
380
 
381
+ @dataclass(kw_only=True, slots=True)
382
+ class ExtractURLOutput:
383
+ username: str
384
+ password: secret_str
385
+ host: str
386
+ port: int
387
+ database: str
388
+
389
+
390
+ def extract_url(url: URL, /) -> ExtractURLOutput:
391
+ """Extract the database, host & port from a URL."""
392
+ if url.username is None:
393
+ raise _ExtractURLUsernameError(url=url)
394
+ if url.password is None:
395
+ raise _ExtractURLPasswordError(url=url)
396
+ if url.host is None:
397
+ raise _ExtractURLHostError(url=url)
398
+ if url.port is None:
399
+ raise _ExtractURLPortError(url=url)
400
+ if url.database is None:
401
+ raise _ExtractURLDatabaseError(url=url)
402
+ return ExtractURLOutput(
403
+ username=url.username,
404
+ password=secret_str(url.password),
405
+ host=url.host,
406
+ port=url.port,
407
+ database=url.database,
408
+ )
409
+
410
+
411
+ @dataclass(kw_only=True, slots=True)
412
+ class ExtractURLError(Exception):
413
+ url: URL
414
+
415
+
416
+ @dataclass(kw_only=True, slots=True)
417
+ class _ExtractURLUsernameError(ExtractURLError):
418
+ @override
419
+ def __str__(self) -> str:
420
+ return f"Expected URL to contain a user name; got {self.url}"
421
+
422
+
423
+ @dataclass(kw_only=True, slots=True)
424
+ class _ExtractURLPasswordError(ExtractURLError):
425
+ @override
426
+ def __str__(self) -> str:
427
+ return f"Expected URL to contain a password; got {self.url}"
428
+
429
+
430
+ @dataclass(kw_only=True, slots=True)
431
+ class _ExtractURLHostError(ExtractURLError):
432
+ @override
433
+ def __str__(self) -> str:
434
+ return f"Expected URL to contain a host; got {self.url}"
435
+
436
+
437
+ @dataclass(kw_only=True, slots=True)
438
+ class _ExtractURLPortError(ExtractURLError):
439
+ @override
440
+ def __str__(self) -> str:
441
+ return f"Expected URL to contain a port; got {self.url}"
442
+
443
+
444
+ @dataclass(kw_only=True, slots=True)
445
+ class _ExtractURLDatabaseError(ExtractURLError):
446
+ @override
447
+ def __str__(self) -> str:
448
+ return f"Expected URL to contain a database; got {self.url}"
449
+
450
+
451
+ ##
452
+
453
+
381
454
  def get_chunk_size(
382
455
  dialect_or_engine_or_conn: DialectOrEngineOrConnectionOrAsync,
383
456
  table_or_orm_or_num_cols: TableOrORMInstOrClass | Sized | int,
@@ -1180,6 +1253,8 @@ __all__ = [
1180
1253
  "CheckEngineError",
1181
1254
  "DialectOrEngineOrConnectionOrAsync",
1182
1255
  "EngineOrConnectionOrAsync",
1256
+ "ExtractURLError",
1257
+ "ExtractURLOutput",
1183
1258
  "GetTableError",
1184
1259
  "InsertItemsError",
1185
1260
  "TablenameMixin",
@@ -1193,6 +1268,7 @@ __all__ = [
1193
1268
  "ensure_tables_dropped",
1194
1269
  "enum_name",
1195
1270
  "enum_values",
1271
+ "extract_url",
1196
1272
  "get_chunk_size",
1197
1273
  "get_column_names",
1198
1274
  "get_columns",
utilities/text.py CHANGED
@@ -9,7 +9,7 @@ from re import IGNORECASE, Match, escape, search
9
9
  from textwrap import dedent
10
10
  from threading import get_ident
11
11
  from time import time_ns
12
- from typing import TYPE_CHECKING, Any, Literal, overload, override
12
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal, overload, override
13
13
  from uuid import uuid4
14
14
 
15
15
  from utilities.iterables import CheckDuplicatesError, check_duplicates, transpose
@@ -385,6 +385,24 @@ def _escape_separator(*, separator: str = DEFAULT_SEPARATOR) -> str:
385
385
  ##
386
386
 
387
387
 
388
+ class secret_str(str): # noqa: N801
389
+ """A string with an obfuscated representation."""
390
+
391
+ __slots__ = ()
392
+ _REPR: ClassVar[str] = "***"
393
+
394
+ @override
395
+ def __repr__(self) -> str:
396
+ return self._REPR
397
+
398
+ @override
399
+ def __str__(self) -> str:
400
+ return self._REPR
401
+
402
+
403
+ ##
404
+
405
+
388
406
  def str_encode(obj: Any, /) -> bytes:
389
407
  """Return the string representation of the object encoded as bytes."""
390
408
  return str(obj).encode()
@@ -424,6 +442,7 @@ __all__ = [
424
442
  "parse_bool",
425
443
  "parse_none",
426
444
  "repr_encode",
445
+ "secret_str",
427
446
  "snake_case",
428
447
  "split_key_value_pairs",
429
448
  "split_str",