corio 2.2.2a4__tar.gz → 2.2.4__tar.gz
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.
- {corio-2.2.2a4 → corio-2.2.4}/PKG-INFO +6 -1
- {corio-2.2.2a4 → corio-2.2.4}/corio/entrypoint.py +13 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/infra/incrementor_pyproject.py +5 -2
- {corio-2.2.2a4 → corio-2.2.4}/corio/infra/releaser.py +21 -11
- {corio-2.2.2a4 → corio-2.2.4}/corio/pyproject.package.toml +9 -5
- corio-2.2.4/corio/tests/test_dns.py +89 -0
- corio-2.2.4/corio/tests/test_dt.py +10 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/tests/test_env.py +10 -0
- corio-2.2.4/corio/tests/test_hash.py +18 -0
- corio-2.2.4/corio/tests/test_hook.py +28 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/tests/test_infra.py +86 -1
- corio-2.2.4/corio/tests/test_iterator.py +48 -0
- corio-2.2.4/corio/tests/test_jsn.py +24 -0
- corio-2.2.4/corio/tests/test_name.py +20 -0
- corio-2.2.4/corio/tests/test_path.py +158 -0
- corio-2.2.4/corio/tests/test_patterns.py +71 -0
- corio-2.2.4/corio/tests/test_rand.py +46 -0
- corio-2.2.4/corio/tests/test_strings.py +54 -0
- corio-2.2.4/corio/tests/test_toml.py +37 -0
- corio-2.2.4/corio/tests/test_tools.py +14 -0
- corio-2.2.4/corio/tests/test_yaml.py +26 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio.egg-info/PKG-INFO +6 -1
- {corio-2.2.2a4 → corio-2.2.4}/corio.egg-info/SOURCES.txt +11 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio.egg-info/requires.txt +7 -0
- {corio-2.2.2a4 → corio-2.2.4}/pyproject.toml +9 -5
- corio-2.2.2a4/corio/tests/test_jsn.py +0 -13
- corio-2.2.2a4/corio/tests/test_path.py +0 -99
- corio-2.2.2a4/corio/tests/test_yaml.py +0 -13
- {corio-2.2.2a4 → corio-2.2.4}/LICENSE +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/README.md +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/ai/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/ai/agentic.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/ai/infer.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/aio.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/api.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/augmentation.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/av.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/caching.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/constants.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/context.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/dataclass.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/datatype.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/db/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/db/document.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/debug.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/dm.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/dns/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/dns/client.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/dns/dm.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/dns/proxy.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/dns/server.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/docker/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/dt.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/encrypt.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/env.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/function.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/google_api.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/ha/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/ha/constants.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/ha/core.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/ha/supervisor.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/ha/utils.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/hash.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/hfh.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/hook.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/https.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/infra/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/infra/api.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/infra/project.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/infra/repository.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/infra/stack.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/inherit.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/inspection.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/interface/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/interface/context.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/interface/controls.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/interface/interface.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/iterator.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/jsn.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/json_fix.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/logs.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/markup.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/merging.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/metric.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/mqtt.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/name.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/net.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/netrc.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/openai.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/parallel.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/path/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/path/app.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/path/path.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/path/type.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/paths.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/patterns.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/pdf.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/plat.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/process.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/profiling.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/rand.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/sec.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/semantic.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/sets.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/setup/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/spaces.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/strings.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/tabular.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/tests/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/tests/conftest.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/tests/helpers.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/tests/test_caching.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/tests/test_datatype.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/tests/test_encrypt.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/tokenization.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/toml.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/tools.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/unicode.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/version/__init__.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/version/version.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/webhook.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/yml.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio/youtube.py +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio.egg-info/dependency_links.txt +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio.egg-info/entry_points.txt +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/corio.egg-info/top_level.txt +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/add-service +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/add-user-path +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/add-ve-shell +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/apt-essentials +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/apt-headless +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/auth-token +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/compose-update +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/compress-dir +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/cru +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/docker-build-bases +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/docker-build-bases-dev +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/docker-install-deps +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/docker-install-prod +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/docker-prune +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/docker-sandbox +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/docker-sandbox-init +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/docs-deploy +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/download +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/encrypt-secrets +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/fmtr-test-script +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/git-clone +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/ha-addon-launch +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/infra +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/infra-sync +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/install-browser +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/install-docker +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/install-ts +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/install-ys +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/mirror-dir +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/opt-dev-init +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/parse-args +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/pypi-check +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/pypi-reserve +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/run-script +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/set-password +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/set-secure-path +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/set-user-sudo +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/snips-install +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/ssh-auth +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/ssh-serve +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/tasmota-config +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/tasmota-flash +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/tasmota-terminal +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/vlc-tn +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/scripts/vm-launch +0 -0
- {corio-2.2.2a4 → corio-2.2.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: corio
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.4
|
|
4
4
|
Summary: Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML
|
|
5
5
|
Author-email: Frontmatter AI <innovative.fowler@mask.pro.fmtr.dev>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -188,6 +188,8 @@ Requires-Dist: httpx; extra == "dns"
|
|
|
188
188
|
Requires-Dist: httpx_retries; extra == "dns"
|
|
189
189
|
Requires-Dist: logfire; extra == "dns"
|
|
190
190
|
Requires-Dist: logfire[httpx]; extra == "dns"
|
|
191
|
+
Requires-Dist: diskcache; extra == "dns"
|
|
192
|
+
Requires-Dist: cachetools; extra == "dns"
|
|
191
193
|
Provides-Extra: patterns
|
|
192
194
|
Requires-Dist: regex; extra == "patterns"
|
|
193
195
|
Provides-Extra: http
|
|
@@ -214,6 +216,9 @@ Requires-Dist: av; extra == "av"
|
|
|
214
216
|
Provides-Extra: env
|
|
215
217
|
Provides-Extra: env-io
|
|
216
218
|
Requires-Dist: dotenv; extra == "env-io"
|
|
219
|
+
Provides-Extra: toml
|
|
220
|
+
Provides-Extra: toml-write
|
|
221
|
+
Requires-Dist: tomlkit; extra == "toml-write"
|
|
217
222
|
Provides-Extra: ha
|
|
218
223
|
Requires-Dist: dotenv; extra == "ha"
|
|
219
224
|
Provides-Extra: ha-api
|
|
@@ -3,6 +3,7 @@ from pydantic_settings import CliSubCommand
|
|
|
3
3
|
from corio import dm
|
|
4
4
|
from corio import sec
|
|
5
5
|
from corio import sets
|
|
6
|
+
from corio.path import Path
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class DocServe(dm.Base):
|
|
@@ -42,6 +43,17 @@ class EpTest(dm.Base):
|
|
|
42
43
|
print("Ran test entrypoint.")
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
class Test(dm.Base):
|
|
47
|
+
name: str = Path.cwd().name
|
|
48
|
+
|
|
49
|
+
def run(self):
|
|
50
|
+
from corio.infra.project import Project
|
|
51
|
+
|
|
52
|
+
project = Project(self.name)
|
|
53
|
+
is_passed = project.releaser.tester.run()
|
|
54
|
+
return int(not is_passed)
|
|
55
|
+
|
|
56
|
+
|
|
45
57
|
class ShellDebug(dm.Base):
|
|
46
58
|
def run(self):
|
|
47
59
|
from corio import debug
|
|
@@ -81,6 +93,7 @@ class Cli(sets.Base, cli_parse_args=True):
|
|
|
81
93
|
secrets: CliSubCommand[sec.Cli]
|
|
82
94
|
docs: CliSubCommand[Docs]
|
|
83
95
|
pyproject: CliSubCommand[Pyproject]
|
|
96
|
+
test: CliSubCommand[Test]
|
|
84
97
|
ep_test: CliSubCommand[EpTest]
|
|
85
98
|
shell_debug: CliSubCommand[ShellDebug]
|
|
86
99
|
cache_hfh: CliSubCommand[CacheHfh]
|
|
@@ -72,8 +72,6 @@ class IncrementorPyproject(Incrementor):
|
|
|
72
72
|
except InvalidRequirement:
|
|
73
73
|
return dep
|
|
74
74
|
|
|
75
|
-
if requirement.specifier:
|
|
76
|
-
return dep
|
|
77
75
|
if requirement.url:
|
|
78
76
|
return dep
|
|
79
77
|
|
|
@@ -83,6 +81,11 @@ class IncrementorPyproject(Incrementor):
|
|
|
83
81
|
if metadata is None:
|
|
84
82
|
return dep
|
|
85
83
|
|
|
84
|
+
if requirement.specifier:
|
|
85
|
+
operators = {specifier.operator for specifier in requirement.specifier}
|
|
86
|
+
if not operators.issubset({"==", "==="}):
|
|
87
|
+
return dep
|
|
88
|
+
|
|
86
89
|
extras = ""
|
|
87
90
|
if requirement.extras:
|
|
88
91
|
extras = f"[{','.join(sorted(requirement.extras))}]"
|
|
@@ -525,6 +525,7 @@ class ReleaseDocumentation(Release):
|
|
|
525
525
|
{"pymdownx.tabbed": {"alternate_style": True}},
|
|
526
526
|
{"pymdownx.emoji": {"emoji_index": twemoji, "emoji_generator": to_svg}},
|
|
527
527
|
],
|
|
528
|
+
exclude_docs="*.hidden.md\n**/*.hidden.md",
|
|
528
529
|
extra={
|
|
529
530
|
"version": {"provider": "mike"},
|
|
530
531
|
},
|
|
@@ -583,13 +584,7 @@ class Tester(Inherit[Releaser]):
|
|
|
583
584
|
@cached_property
|
|
584
585
|
def dependencies(self) -> dict[str, list[str]]:
|
|
585
586
|
data = self.paths.pyproject_repo.read_toml()
|
|
586
|
-
|
|
587
|
-
dependencies = {
|
|
588
|
-
str(key): [str(value) for value in values]
|
|
589
|
-
for key, values in table.items()
|
|
590
|
-
if isinstance(values, list)
|
|
591
|
-
}
|
|
592
|
-
return dependencies
|
|
587
|
+
return data["project"].get("optional-dependencies",{})
|
|
593
588
|
|
|
594
589
|
@cached_property
|
|
595
590
|
def modules(self) -> list[str]:
|
|
@@ -607,9 +602,11 @@ class Tester(Inherit[Releaser]):
|
|
|
607
602
|
def get_env(self, name: str, path_tests: Path, extras: list[str]) -> dict:
|
|
608
603
|
if path_tests.is_relative_to(self.paths.repo):
|
|
609
604
|
path_tests = path_tests.relative_to(self.paths.repo)
|
|
605
|
+
deps = self.get_deps_extras(extras)
|
|
610
606
|
env = {
|
|
611
607
|
"description": f"Run {name} tests.",
|
|
612
608
|
"extras": extras,
|
|
609
|
+
"deps": deps,
|
|
613
610
|
"commands": [["python", "-m", "pytest", "-q", str(path_tests)]],
|
|
614
611
|
}
|
|
615
612
|
return env
|
|
@@ -618,6 +615,22 @@ class Tester(Inherit[Releaser]):
|
|
|
618
615
|
def env(self) -> dict:
|
|
619
616
|
return self.get_env(name=self.paths.name_ns, path_tests=self.paths.tests, extras=["test"])
|
|
620
617
|
|
|
618
|
+
def get_extras_module(self, module: str) -> list[str]:
|
|
619
|
+
extras = ["test"]
|
|
620
|
+
extras_available = set(self.dependencies.keys())
|
|
621
|
+
|
|
622
|
+
if module in extras_available:
|
|
623
|
+
extras.insert(0, module)
|
|
624
|
+
|
|
625
|
+
extras_children = sorted(extra for extra in extras_available if extra.startswith(f"{module}."))
|
|
626
|
+
return extras_children + extras
|
|
627
|
+
|
|
628
|
+
def get_deps_extras(self, extras: list[str]) -> list[str]:
|
|
629
|
+
resolved = []
|
|
630
|
+
for extra in extras:
|
|
631
|
+
resolved.extend(self.dependencies.get(extra, []))
|
|
632
|
+
return list(dict.fromkeys(resolved))
|
|
633
|
+
|
|
621
634
|
@cached_property
|
|
622
635
|
def envs(self) -> dict[str, dict]:
|
|
623
636
|
if not self.paths.metadata.test_envs:
|
|
@@ -626,12 +639,9 @@ class Tester(Inherit[Releaser]):
|
|
|
626
639
|
return {self.paths.name_ns: self.env}
|
|
627
640
|
|
|
628
641
|
envs = {}
|
|
629
|
-
extras_available = set(self.dependencies.keys())
|
|
630
642
|
|
|
631
643
|
for module in self.modules:
|
|
632
|
-
extras =
|
|
633
|
-
if module in extras_available:
|
|
634
|
-
extras.insert(0, module)
|
|
644
|
+
extras = self.get_extras_module(module)
|
|
635
645
|
|
|
636
646
|
path_test = self.paths.tests / f"{self.TEST_FILENAME_PREFIX}{module}{self.TEST_FILENAME_SUFFIX}"
|
|
637
647
|
name = f"{self.paths.name_ns}.{module}"
|
|
@@ -40,7 +40,7 @@ debug = ["pydevd-pycharm~=251.25410.159"]
|
|
|
40
40
|
sets = ["pydantic-settings", "dm", "yaml"]
|
|
41
41
|
"path.app" = ["appdirs"]
|
|
42
42
|
"path.type" = ["filetype"]
|
|
43
|
-
dns = ["dnspython[doh]", "http"]
|
|
43
|
+
dns = ["dnspython[doh]", "http", "caching"]
|
|
44
44
|
patterns = ["regex"]
|
|
45
45
|
http = ["httpx", "httpx_retries", "logging", "logfire[httpx]"]
|
|
46
46
|
setup = ["setuptools"]
|
|
@@ -52,11 +52,13 @@ mqtt = ["aiomqtt"]
|
|
|
52
52
|
av = ["av"]
|
|
53
53
|
env = []
|
|
54
54
|
"env.io" = ["dotenv"]
|
|
55
|
+
toml = []
|
|
56
|
+
"toml.write" = ["tomlkit"]
|
|
55
57
|
ha = ["env.io"]
|
|
56
58
|
"ha.api" = ["ha", "homeassistant_api", "aiohasupervisor"]
|
|
57
59
|
doc = ["mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav"]
|
|
58
60
|
youtube = ["pytubefix"]
|
|
59
|
-
infra = ["version.dev", "logging", "setup", "doc", "sets", "build", "twine", "packaging", "vcs", "docker.client", "merging", "http", "api", "
|
|
61
|
+
infra = ["version.dev", "logging", "setup", "doc", "sets", "build", "twine", "packaging", "vcs", "docker.client", "merging", "http", "api", "toml.write", "secrets", "cli", "test"]
|
|
60
62
|
vcs = ["pygit2"]
|
|
61
63
|
tasmota = ["decode-config", "esptool"]
|
|
62
64
|
encrypt = ["pyrage"]
|
|
@@ -64,7 +66,7 @@ secrets = ["encrypt", "env.io", "yaml", "logging", "sets", "vcs"]
|
|
|
64
66
|
cli = ["sets", "logging"]
|
|
65
67
|
|
|
66
68
|
[tool.corio.metadata]
|
|
67
|
-
version = "2.2.
|
|
69
|
+
version = "2.2.4"
|
|
68
70
|
port = 0
|
|
69
71
|
base = "python"
|
|
70
72
|
description = "Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML"
|
|
@@ -94,7 +96,7 @@ corio = ["pyproject.package.toml"]
|
|
|
94
96
|
|
|
95
97
|
[project]
|
|
96
98
|
name = "corio"
|
|
97
|
-
version = "2.2.
|
|
99
|
+
version = "2.2.4"
|
|
98
100
|
description = "Collection of high-level tools to simplify everyday development tasks, with a focus on AI/ML"
|
|
99
101
|
readme = "README.md"
|
|
100
102
|
dependencies = []
|
|
@@ -139,7 +141,7 @@ debug = ["pydevd-pycharm~=251.25410.159"]
|
|
|
139
141
|
sets = ["pydantic-settings", "pydantic", "pydantic-extra-types", "pycountry", "yamlscript", "pyyaml"]
|
|
140
142
|
"path.app" = ["appdirs"]
|
|
141
143
|
"path.type" = ["filetype"]
|
|
142
|
-
dns = ["dnspython[doh]", "httpx", "httpx_retries", "logfire", "logfire[httpx]"]
|
|
144
|
+
dns = ["dnspython[doh]", "httpx", "httpx_retries", "logfire", "logfire[httpx]", "diskcache", "cachetools"]
|
|
143
145
|
patterns = ["regex"]
|
|
144
146
|
http = ["httpx", "httpx_retries", "logfire", "logfire[httpx]"]
|
|
145
147
|
setup = ["setuptools"]
|
|
@@ -151,6 +153,8 @@ mqtt = ["aiomqtt"]
|
|
|
151
153
|
av = ["av"]
|
|
152
154
|
env = []
|
|
153
155
|
"env.io" = ["dotenv"]
|
|
156
|
+
toml = []
|
|
157
|
+
"toml.write" = ["tomlkit"]
|
|
154
158
|
ha = ["dotenv"]
|
|
155
159
|
"ha.api" = ["dotenv", "homeassistant_api", "aiohasupervisor"]
|
|
156
160
|
doc = ["mkdocs", "mkdocs-material", "mkdocstrings[python]", "mike", "mkdocs-include-dir-to-nav"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import dns
|
|
2
|
+
from dns import rcode as dnspython_rcode
|
|
3
|
+
|
|
4
|
+
from corio.dns import client as dns_client
|
|
5
|
+
from corio.dns import dm as dns_dm
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _make_exchange(name: str = "example.com.", rdtype=dns.rdatatype.A) -> dns_dm.Exchange:
|
|
9
|
+
query = dns.message.make_query(name, rdtype)
|
|
10
|
+
return dns_dm.Exchange.from_wire(query.to_wire(), ip="127.0.0.1", port=5353)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_response_ttl_from_answers_authority_and_rcode_defaults():
|
|
14
|
+
exchange = _make_exchange()
|
|
15
|
+
message = exchange.request.get_response_template()
|
|
16
|
+
message.answer.append(dns.rrset.from_text("example.com.", 300, "IN", "A", "1.1.1.1"))
|
|
17
|
+
message.answer.append(dns.rrset.from_text("example.com.", 120, "IN", "A", "1.1.1.2"))
|
|
18
|
+
response = dns_dm.Response.from_message(message)
|
|
19
|
+
assert response.ttl == 120
|
|
20
|
+
|
|
21
|
+
message = exchange.request.get_response_template()
|
|
22
|
+
message.authority.append(dns.rrset.from_text("example.com.", 42, "IN", "NS", "ns1.example.com."))
|
|
23
|
+
response = dns_dm.Response.from_message(message)
|
|
24
|
+
assert response.ttl == 42
|
|
25
|
+
|
|
26
|
+
message = exchange.request.get_response_template()
|
|
27
|
+
message.set_rcode(dnspython_rcode.SERVFAIL)
|
|
28
|
+
response = dns_dm.Response.from_message(message)
|
|
29
|
+
assert response.ttl == 10
|
|
30
|
+
|
|
31
|
+
response = dns_dm.Response.from_message(message, ttl_defaults={"SERVFAIL": 77})
|
|
32
|
+
assert response.ttl == 77
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_exchange_question_last_and_query_last_use_latest_answer_name():
|
|
36
|
+
exchange = _make_exchange(name="example.com.")
|
|
37
|
+
message = exchange.request.get_response_template()
|
|
38
|
+
message.answer.append(dns.rrset.from_text("edge.example.com.", 60, "IN", "A", "9.9.9.9"))
|
|
39
|
+
exchange.response = dns_dm.Response.from_message(message)
|
|
40
|
+
|
|
41
|
+
question_last = exchange.question_last
|
|
42
|
+
assert question_last.name.to_text() == "9.9.9.9"
|
|
43
|
+
assert question_last.rdtype == exchange.request.type
|
|
44
|
+
|
|
45
|
+
query_last = exchange.query_last
|
|
46
|
+
assert query_last.question[0].name.to_text() == "9.9.9.9"
|
|
47
|
+
assert query_last.id == exchange.request.message.id
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_exchange_reverse_builds_internal_ptr_query():
|
|
51
|
+
exchange = _make_exchange()
|
|
52
|
+
reverse = exchange.reverse
|
|
53
|
+
|
|
54
|
+
assert reverse.is_internal is True
|
|
55
|
+
assert reverse.ip == exchange.ip
|
|
56
|
+
assert reverse.port == exchange.port
|
|
57
|
+
assert reverse.request.type_text == "PTR"
|
|
58
|
+
assert reverse.request.name_text.endswith(".in-addr.arpa.")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_plain_client_resolve_applies_ttl_min(monkeypatch):
|
|
62
|
+
exchange = _make_exchange()
|
|
63
|
+
upstream = exchange.request.get_response_template()
|
|
64
|
+
upstream.answer.append(dns.rrset.from_text("example.com.", 3, "IN", "A", "1.1.1.1"))
|
|
65
|
+
|
|
66
|
+
monkeypatch.setattr(dns_client.dnspython_query, "udp", lambda q, where, port: upstream)
|
|
67
|
+
|
|
68
|
+
client_plain = dns_client.Plain(host="8.8.8.8", ttl_min=30)
|
|
69
|
+
client_plain.resolve(exchange)
|
|
70
|
+
|
|
71
|
+
assert exchange.response.answer is not None
|
|
72
|
+
assert exchange.response.answer.ttl == 30
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_http_client_resolve_sets_servfail_on_exception(monkeypatch):
|
|
76
|
+
exchange = _make_exchange()
|
|
77
|
+
client_http = dns_client.HTTP(host="dns.google", url="https://{host}/dns-query")
|
|
78
|
+
client_http.__dict__["ip"] = "1.1.1.1"
|
|
79
|
+
|
|
80
|
+
def _raise(*_args, **_kwargs):
|
|
81
|
+
raise RuntimeError("boom")
|
|
82
|
+
|
|
83
|
+
monkeypatch.setattr(dns_client, "logger", type("L", (), {"exception": staticmethod(lambda *_args, **_kwargs: None)})())
|
|
84
|
+
monkeypatch.setattr(client_http.CLIENT, "post", _raise)
|
|
85
|
+
|
|
86
|
+
client_http.resolve(exchange)
|
|
87
|
+
|
|
88
|
+
assert exchange.response.rcode == dnspython_rcode.SERVFAIL
|
|
89
|
+
assert exchange.response.is_complete is True
|
|
@@ -73,3 +73,13 @@ def test_get_env_dict():
|
|
|
73
73
|
with helpers.patch_environment(clear=True, **ENVIRONMENT_DATA):
|
|
74
74
|
actual = env.get_dict()
|
|
75
75
|
assert actual == expected
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_env_file_round_trip_via_path_data(tmp_path):
|
|
79
|
+
path_env = Path(tmp_path / ".env")
|
|
80
|
+
expected = {"A": "1", "B": "two"}
|
|
81
|
+
|
|
82
|
+
path_env.write_data(expected)
|
|
83
|
+
actual = path_env.read_data()
|
|
84
|
+
|
|
85
|
+
assert actual == expected
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from corio import hash as hash_module
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_hash_unit_is_stable_and_bounded():
|
|
5
|
+
first = hash_module.hash_unit("corio")
|
|
6
|
+
second = hash_module.hash_unit("corio")
|
|
7
|
+
|
|
8
|
+
assert first == second
|
|
9
|
+
assert 0.0 <= first < 1.0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_get_hash_readable_length_and_replacements():
|
|
13
|
+
value = hash_module.get_hash_readable("corio", length=16)
|
|
14
|
+
|
|
15
|
+
assert len(value) == 16
|
|
16
|
+
assert "O" not in value
|
|
17
|
+
assert "I" not in value
|
|
18
|
+
assert "=" not in value
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from corio.hook import ImportHook, MissingExtraError, MissingExtraMockModule
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_missing_extra_mock_module_raises_with_context():
|
|
7
|
+
mock_module = MissingExtraMockModule("path.app", ModuleNotFoundError("no appdirs"))
|
|
8
|
+
|
|
9
|
+
with pytest.raises(MissingExtraError):
|
|
10
|
+
mock_module()
|
|
11
|
+
|
|
12
|
+
with pytest.raises(MissingExtraError):
|
|
13
|
+
_ = mock_module.any_attr
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_import_hook_translates_module_not_found_for_corio_callers():
|
|
17
|
+
hook = ImportHook(auto_register=False)
|
|
18
|
+
|
|
19
|
+
def fake_import(*_args, **_kwargs):
|
|
20
|
+
raise ModuleNotFoundError("missing dep")
|
|
21
|
+
|
|
22
|
+
hook._previous_import = fake_import
|
|
23
|
+
|
|
24
|
+
with pytest.raises(MissingExtraError):
|
|
25
|
+
hook("missing", globals={"__name__": "corio.path.app"})
|
|
26
|
+
|
|
27
|
+
with pytest.raises(ModuleNotFoundError):
|
|
28
|
+
hook("missing", globals={"__name__": "external.module"})
|
|
@@ -3,6 +3,7 @@ from types import SimpleNamespace
|
|
|
3
3
|
from packaging.requirements import Requirement
|
|
4
4
|
|
|
5
5
|
from corio.infra.incrementor_pyproject import IncrementorPyproject
|
|
6
|
+
from corio.infra.releaser import Tester as ReleaserTester
|
|
6
7
|
from corio.path import Path
|
|
7
8
|
|
|
8
9
|
|
|
@@ -95,7 +96,7 @@ def test_pin_editables_preserves_extras_and_skips_existing_specifiers(tmp_path):
|
|
|
95
96
|
assert req.extras == {"version.dev", "logging", "sets", "yaml", "debug", "caching", "api", "mqtt"}
|
|
96
97
|
assert str(req.specifier) == "==1.2.3"
|
|
97
98
|
|
|
98
|
-
assert incrementor._pin_editable("haco==1.0.0") == "haco==1.
|
|
99
|
+
assert incrementor._pin_editable("haco==1.0.0") == "haco==1.2.3"
|
|
99
100
|
assert incrementor._pin_editable("haco>=1.0.0") == "haco>=1.0.0"
|
|
100
101
|
|
|
101
102
|
|
|
@@ -161,3 +162,87 @@ def test_process_deps_pins_project_dependencies(tmp_path):
|
|
|
161
162
|
assert incrementor._process_deps(dependencies) == ["haco==1.2.3", "requests>=2"]
|
|
162
163
|
assert incrementor._process_deps(optional_dev) == ["haco[logging]==1.2.3", "pytest"]
|
|
163
164
|
|
|
165
|
+
|
|
166
|
+
def _make_tester(path_repo: Path, *, test_envs: bool) -> ReleaserTester:
|
|
167
|
+
path_tests = path_repo / "corio" / "tests"
|
|
168
|
+
path_tests.mkdir(parents=True)
|
|
169
|
+
path_pyproject = path_repo / "pyproject.toml"
|
|
170
|
+
path_pyproject.write_text(
|
|
171
|
+
"\n".join(
|
|
172
|
+
[
|
|
173
|
+
"[project]",
|
|
174
|
+
'name = "corio"',
|
|
175
|
+
'version = "0.0.0"',
|
|
176
|
+
"",
|
|
177
|
+
"[project.optional-dependencies]",
|
|
178
|
+
'test = ["pytest", "pytest-cov"]',
|
|
179
|
+
'path = []',
|
|
180
|
+
'"path.app" = ["appdirs"]',
|
|
181
|
+
'"path.type" = ["filetype"]',
|
|
182
|
+
"strings = []",
|
|
183
|
+
"",
|
|
184
|
+
]
|
|
185
|
+
),
|
|
186
|
+
encoding="utf-8",
|
|
187
|
+
)
|
|
188
|
+
parent = SimpleNamespace(
|
|
189
|
+
name="corio",
|
|
190
|
+
paths=SimpleNamespace(
|
|
191
|
+
repo=path_repo,
|
|
192
|
+
tests=path_tests,
|
|
193
|
+
pyproject_repo=path_pyproject,
|
|
194
|
+
name_ns="corio",
|
|
195
|
+
metadata=SimpleNamespace(test_envs=test_envs),
|
|
196
|
+
),
|
|
197
|
+
)
|
|
198
|
+
return ReleaserTester(parent)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_tester_get_extras_module_merges_module_and_dotted_children(tmp_path):
|
|
202
|
+
path_repo = Path(tmp_path / "corio")
|
|
203
|
+
path_repo.mkdir(parents=True)
|
|
204
|
+
tester = _make_tester(path_repo, test_envs=True)
|
|
205
|
+
|
|
206
|
+
assert tester.get_extras_module("path") == ["path.app", "path.type", "path", "test"]
|
|
207
|
+
assert tester.get_extras_module("strings") == ["strings", "test"]
|
|
208
|
+
assert tester.get_extras_module("missing") == ["test"]
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_tester_get_deps_extras_resolves_recursive_dependencies(tmp_path):
|
|
212
|
+
path_repo = Path(tmp_path / "corio")
|
|
213
|
+
path_repo.mkdir(parents=True)
|
|
214
|
+
tester = _make_tester(path_repo, test_envs=True)
|
|
215
|
+
|
|
216
|
+
deps = tester.get_deps_extras(["path.app", "path.type", "test"])
|
|
217
|
+
assert "pytest" in deps
|
|
218
|
+
assert "pytest-cov" in deps
|
|
219
|
+
|
|
220
|
+
deps_missing = tester.get_deps_extras(["missing"])
|
|
221
|
+
assert deps_missing == []
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_tester_envs_use_file_module_name_with_test_envs(tmp_path):
|
|
225
|
+
path_repo = Path(tmp_path / "corio")
|
|
226
|
+
path_repo.mkdir(parents=True)
|
|
227
|
+
tester = _make_tester(path_repo, test_envs=True)
|
|
228
|
+
(tester.paths.tests / "test_path.py").write_text("", encoding="utf-8")
|
|
229
|
+
(tester.paths.tests / "test_strings.py").write_text("", encoding="utf-8")
|
|
230
|
+
|
|
231
|
+
envs = tester.envs
|
|
232
|
+
|
|
233
|
+
assert set(envs) == {"corio.path", "corio.strings"}
|
|
234
|
+
assert envs["corio.path"]["extras"] == ["path.app", "path.type", "path", "test"]
|
|
235
|
+
assert envs["corio.strings"]["extras"] == ["strings", "test"]
|
|
236
|
+
assert "pytest" in envs["corio.path"]["deps"]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_tester_envs_fall_back_to_single_env_when_test_envs_disabled(tmp_path):
|
|
240
|
+
path_repo = Path(tmp_path / "corio")
|
|
241
|
+
path_repo.mkdir(parents=True)
|
|
242
|
+
tester = _make_tester(path_repo, test_envs=False)
|
|
243
|
+
(tester.paths.tests / "test_path.py").write_text("", encoding="utf-8")
|
|
244
|
+
|
|
245
|
+
envs = tester.envs
|
|
246
|
+
|
|
247
|
+
assert set(envs) == {"corio"}
|
|
248
|
+
assert envs["corio"]["extras"] == ["test"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from corio import iterator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_enlist_and_dedupe():
|
|
7
|
+
assert iterator.enlist("x") == ["x"]
|
|
8
|
+
assert iterator.enlist(["x"]) == ["x"]
|
|
9
|
+
assert iterator.dedupe(["a", "b", "a", "c"]) == ["a", "b", "c"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_dict_records_to_lists_and_chunking():
|
|
13
|
+
data = [{"a": 1}, {"b": 2}]
|
|
14
|
+
as_lists = iterator.dict_records_to_lists(data, missing=None)
|
|
15
|
+
assert as_lists == {"a": [1, None], "b": [None, 2]}
|
|
16
|
+
|
|
17
|
+
assert iterator.chunk_data([1, 2, 3, 4, 5], size=2) == [[1, 2], [3, 4], [5]]
|
|
18
|
+
assert iterator.get_batch_sizes(total=10, num_batches=3) == [4, 3, 3]
|
|
19
|
+
assert list(iterator.rebatch([[1, 2], [3], [4, 5]], size=2)) == [(1, 2), (3, 4), (5,)]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_strip_none_flatten_tree_and_iterdiffer():
|
|
23
|
+
assert iterator.strip_none(1, None, 2) == [1, 2]
|
|
24
|
+
|
|
25
|
+
tree = {"a": {"b": 1}, "list": [2, 3]}
|
|
26
|
+
flat = iterator.flatten_tree(tree, sep=".")
|
|
27
|
+
assert flat["a.b"] == 1
|
|
28
|
+
assert flat["list.[0]"] == 2
|
|
29
|
+
assert flat["list.[1]"] == 3
|
|
30
|
+
|
|
31
|
+
diff = iterator.IterDiffer(before=[1, 2], after=[2, 3])
|
|
32
|
+
assert diff.added == {3}
|
|
33
|
+
assert diff.removed == {1}
|
|
34
|
+
assert diff.is_changed is True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class _Obj:
|
|
39
|
+
key: str
|
|
40
|
+
value: int
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_index_list_lookup_by_attr_and_key():
|
|
44
|
+
objects = iterator.IndexList([_Obj(key="a", value=1), _Obj(key="b", value=2)])
|
|
45
|
+
assert objects.key["a"].value == 1
|
|
46
|
+
|
|
47
|
+
dicts = iterator.IndexList([{"id": "x", "value": 1}, {"id": "y", "value": 2}])
|
|
48
|
+
assert dicts.id["y"]["value"] == 2
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from corio import jsn
|
|
2
|
+
from corio.path import Path
|
|
3
|
+
from corio.tests.helpers import SERIALIZATION_DATA
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_json():
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
Simple YAML round trip test
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
expected = SERIALIZATION_DATA
|
|
13
|
+
actual = jsn.from_json(jsn.to_json(expected))
|
|
14
|
+
assert actual == expected
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_json_path_round_trip(tmp_path):
|
|
18
|
+
expected = SERIALIZATION_DATA
|
|
19
|
+
path_json = Path(tmp_path / "serialization_test.json")
|
|
20
|
+
|
|
21
|
+
path_json.write_json(expected)
|
|
22
|
+
actual = path_json.read_json()
|
|
23
|
+
|
|
24
|
+
assert actual == expected
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
from corio import name
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_name_lists_are_non_empty():
|
|
7
|
+
assert len(name.get_left()) > 0
|
|
8
|
+
assert len(name.get_right()) > 0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_get_name_tuple_or_string():
|
|
12
|
+
random.seed(0)
|
|
13
|
+
left_right = name.get(sep=None)
|
|
14
|
+
assert isinstance(left_right, tuple)
|
|
15
|
+
assert len(left_right) == 2
|
|
16
|
+
|
|
17
|
+
random.seed(0)
|
|
18
|
+
text = name.get(sep="-")
|
|
19
|
+
assert isinstance(text, str)
|
|
20
|
+
assert "-" in text
|