ArtinKit 0.1.0__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.
- artinkit-0.1.0/LICENSE +21 -0
- artinkit-0.1.0/PKG-INFO +105 -0
- artinkit-0.1.0/README.md +77 -0
- artinkit-0.1.0/pyproject.toml +45 -0
- artinkit-0.1.0/setup.cfg +4 -0
- artinkit-0.1.0/src/ArtinKit.egg-info/PKG-INFO +105 -0
- artinkit-0.1.0/src/ArtinKit.egg-info/SOURCES.txt +17 -0
- artinkit-0.1.0/src/ArtinKit.egg-info/dependency_links.txt +1 -0
- artinkit-0.1.0/src/ArtinKit.egg-info/entry_points.txt +2 -0
- artinkit-0.1.0/src/ArtinKit.egg-info/requires.txt +6 -0
- artinkit-0.1.0/src/ArtinKit.egg-info/top_level.txt +1 -0
- artinkit-0.1.0/src/artinkit/__init__.py +8 -0
- artinkit-0.1.0/src/artinkit/cli.py +40 -0
- artinkit-0.1.0/src/artinkit/files.py +51 -0
- artinkit-0.1.0/src/artinkit/jsonstore.py +32 -0
- artinkit-0.1.0/src/artinkit/logging_utils.py +14 -0
- artinkit-0.1.0/src/artinkit/text.py +51 -0
- artinkit-0.1.0/src/artinkit/validation.py +33 -0
- artinkit-0.1.0/tests/test_core.py +81 -0
artinkit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Artin Karimian
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
artinkit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ArtinKit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight Python utilities with secure defaults and zero mandatory third-party dependencies.
|
|
5
|
+
Author: Artin Karimian
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: utilities,toolkit,json,files,text,cli
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
25
|
+
Requires-Dist: bandit>=1.7; extra == "dev"
|
|
26
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# ArtinKit
|
|
30
|
+
|
|
31
|
+
Lightweight Python utilities with secure defaults and **zero mandatory third-party runtime dependencies**.
|
|
32
|
+
|
|
33
|
+
> ArtinKit is a general-purpose utility toolkit, not a bundled AI model.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- Text helpers: whitespace normalization, word counting, safe truncation, and slug creation.
|
|
38
|
+
- File helpers: UTF-8 reads/writes, directory creation, file listing, and file sizes.
|
|
39
|
+
- JSON storage: Unicode-friendly JSON load/save with clear error behavior.
|
|
40
|
+
- Validation helpers: safe checks for common strings and numeric values.
|
|
41
|
+
- Logging helper: create a consistently configured standard-library logger.
|
|
42
|
+
- CLI: inspect files and normalize text from a terminal.
|
|
43
|
+
- Automated tests, security scanning, and build verification.
|
|
44
|
+
|
|
45
|
+
## Install online
|
|
46
|
+
|
|
47
|
+
After the first release is published on PyPI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python -m pip install ArtinKit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The package name on package indexes is case-insensitive; import it as `artinkit`.
|
|
54
|
+
|
|
55
|
+
## Install offline
|
|
56
|
+
|
|
57
|
+
Download the prebuilt wheel from this repository's release/files section or obtain the wheel from the project maintainer, then install it without internet:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python -m pip install --no-index --no-deps ArtinKit-0.1.0-py3-none-any.whl
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
ArtinKit has no mandatory third-party runtime dependencies. See [OFFLINE_INSTALL.md](OFFLINE_INSTALL.md) for full offline installation instructions.
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from artinkit import text, files, jsonstore
|
|
69
|
+
|
|
70
|
+
print(text.normalize_whitespace(" Hello ArtinKit "))
|
|
71
|
+
print(text.slugify("ArtinKit Utilities"))
|
|
72
|
+
|
|
73
|
+
files.write_text("hello.txt", "Hello from ArtinKit", create_parents=True)
|
|
74
|
+
jsonstore.save_json("settings.json", {"language": "fa", "ready": True})
|
|
75
|
+
settings = jsonstore.load_json("settings.json")
|
|
76
|
+
print(settings)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## CLI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
artinkit normalize " Hello world "
|
|
83
|
+
artinkit file-info README.md
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Security notes
|
|
87
|
+
|
|
88
|
+
- No network access or subprocess execution is performed by the core package.
|
|
89
|
+
- File operations target paths explicitly supplied by the caller.
|
|
90
|
+
- JSON loading does not execute code.
|
|
91
|
+
- Avoid passing untrusted paths to file-writing functions when your application has broader filesystem permissions.
|
|
92
|
+
- Security scans and tests run in GitHub Actions. Publishing is separated into a protected job that uses PyPI Trusted Publishing.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python -m pip install -e ".[dev]"
|
|
98
|
+
python -m pytest
|
|
99
|
+
python -m build
|
|
100
|
+
python -m twine check dist/*
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT. See [LICENSE](LICENSE).
|
artinkit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# ArtinKit
|
|
2
|
+
|
|
3
|
+
Lightweight Python utilities with secure defaults and **zero mandatory third-party runtime dependencies**.
|
|
4
|
+
|
|
5
|
+
> ArtinKit is a general-purpose utility toolkit, not a bundled AI model.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Text helpers: whitespace normalization, word counting, safe truncation, and slug creation.
|
|
10
|
+
- File helpers: UTF-8 reads/writes, directory creation, file listing, and file sizes.
|
|
11
|
+
- JSON storage: Unicode-friendly JSON load/save with clear error behavior.
|
|
12
|
+
- Validation helpers: safe checks for common strings and numeric values.
|
|
13
|
+
- Logging helper: create a consistently configured standard-library logger.
|
|
14
|
+
- CLI: inspect files and normalize text from a terminal.
|
|
15
|
+
- Automated tests, security scanning, and build verification.
|
|
16
|
+
|
|
17
|
+
## Install online
|
|
18
|
+
|
|
19
|
+
After the first release is published on PyPI:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
python -m pip install ArtinKit
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The package name on package indexes is case-insensitive; import it as `artinkit`.
|
|
26
|
+
|
|
27
|
+
## Install offline
|
|
28
|
+
|
|
29
|
+
Download the prebuilt wheel from this repository's release/files section or obtain the wheel from the project maintainer, then install it without internet:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python -m pip install --no-index --no-deps ArtinKit-0.1.0-py3-none-any.whl
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
ArtinKit has no mandatory third-party runtime dependencies. See [OFFLINE_INSTALL.md](OFFLINE_INSTALL.md) for full offline installation instructions.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from artinkit import text, files, jsonstore
|
|
41
|
+
|
|
42
|
+
print(text.normalize_whitespace(" Hello ArtinKit "))
|
|
43
|
+
print(text.slugify("ArtinKit Utilities"))
|
|
44
|
+
|
|
45
|
+
files.write_text("hello.txt", "Hello from ArtinKit", create_parents=True)
|
|
46
|
+
jsonstore.save_json("settings.json", {"language": "fa", "ready": True})
|
|
47
|
+
settings = jsonstore.load_json("settings.json")
|
|
48
|
+
print(settings)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
artinkit normalize " Hello world "
|
|
55
|
+
artinkit file-info README.md
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Security notes
|
|
59
|
+
|
|
60
|
+
- No network access or subprocess execution is performed by the core package.
|
|
61
|
+
- File operations target paths explicitly supplied by the caller.
|
|
62
|
+
- JSON loading does not execute code.
|
|
63
|
+
- Avoid passing untrusted paths to file-writing functions when your application has broader filesystem permissions.
|
|
64
|
+
- Security scans and tests run in GitHub Actions. Publishing is separated into a protected job that uses PyPI Trusted Publishing.
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python -m pip install -e ".[dev]"
|
|
70
|
+
python -m pytest
|
|
71
|
+
python -m build
|
|
72
|
+
python -m twine check dist/*
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ArtinKit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lightweight Python utilities with secure defaults and zero mandatory third-party dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Artin Karimian" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["utilities", "toolkit", "json", "files", "text", "cli"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
]
|
|
29
|
+
dependencies = []
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["build>=1.2", "pytest>=8", "bandit>=1.7", "twine>=5"]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
artinkit = "artinkit.cli:main"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
addopts = "-ra"
|
|
43
|
+
|
|
44
|
+
[tool.bandit]
|
|
45
|
+
exclude_dirs = ["tests"]
|
artinkit-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ArtinKit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight Python utilities with secure defaults and zero mandatory third-party dependencies.
|
|
5
|
+
Author: Artin Karimian
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: utilities,toolkit,json,files,text,cli
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
25
|
+
Requires-Dist: bandit>=1.7; extra == "dev"
|
|
26
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# ArtinKit
|
|
30
|
+
|
|
31
|
+
Lightweight Python utilities with secure defaults and **zero mandatory third-party runtime dependencies**.
|
|
32
|
+
|
|
33
|
+
> ArtinKit is a general-purpose utility toolkit, not a bundled AI model.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- Text helpers: whitespace normalization, word counting, safe truncation, and slug creation.
|
|
38
|
+
- File helpers: UTF-8 reads/writes, directory creation, file listing, and file sizes.
|
|
39
|
+
- JSON storage: Unicode-friendly JSON load/save with clear error behavior.
|
|
40
|
+
- Validation helpers: safe checks for common strings and numeric values.
|
|
41
|
+
- Logging helper: create a consistently configured standard-library logger.
|
|
42
|
+
- CLI: inspect files and normalize text from a terminal.
|
|
43
|
+
- Automated tests, security scanning, and build verification.
|
|
44
|
+
|
|
45
|
+
## Install online
|
|
46
|
+
|
|
47
|
+
After the first release is published on PyPI:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python -m pip install ArtinKit
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The package name on package indexes is case-insensitive; import it as `artinkit`.
|
|
54
|
+
|
|
55
|
+
## Install offline
|
|
56
|
+
|
|
57
|
+
Download the prebuilt wheel from this repository's release/files section or obtain the wheel from the project maintainer, then install it without internet:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python -m pip install --no-index --no-deps ArtinKit-0.1.0-py3-none-any.whl
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
ArtinKit has no mandatory third-party runtime dependencies. See [OFFLINE_INSTALL.md](OFFLINE_INSTALL.md) for full offline installation instructions.
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from artinkit import text, files, jsonstore
|
|
69
|
+
|
|
70
|
+
print(text.normalize_whitespace(" Hello ArtinKit "))
|
|
71
|
+
print(text.slugify("ArtinKit Utilities"))
|
|
72
|
+
|
|
73
|
+
files.write_text("hello.txt", "Hello from ArtinKit", create_parents=True)
|
|
74
|
+
jsonstore.save_json("settings.json", {"language": "fa", "ready": True})
|
|
75
|
+
settings = jsonstore.load_json("settings.json")
|
|
76
|
+
print(settings)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## CLI
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
artinkit normalize " Hello world "
|
|
83
|
+
artinkit file-info README.md
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Security notes
|
|
87
|
+
|
|
88
|
+
- No network access or subprocess execution is performed by the core package.
|
|
89
|
+
- File operations target paths explicitly supplied by the caller.
|
|
90
|
+
- JSON loading does not execute code.
|
|
91
|
+
- Avoid passing untrusted paths to file-writing functions when your application has broader filesystem permissions.
|
|
92
|
+
- Security scans and tests run in GitHub Actions. Publishing is separated into a protected job that uses PyPI Trusted Publishing.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
python -m pip install -e ".[dev]"
|
|
98
|
+
python -m pytest
|
|
99
|
+
python -m build
|
|
100
|
+
python -m twine check dist/*
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/ArtinKit.egg-info/PKG-INFO
|
|
5
|
+
src/ArtinKit.egg-info/SOURCES.txt
|
|
6
|
+
src/ArtinKit.egg-info/dependency_links.txt
|
|
7
|
+
src/ArtinKit.egg-info/entry_points.txt
|
|
8
|
+
src/ArtinKit.egg-info/requires.txt
|
|
9
|
+
src/ArtinKit.egg-info/top_level.txt
|
|
10
|
+
src/artinkit/__init__.py
|
|
11
|
+
src/artinkit/cli.py
|
|
12
|
+
src/artinkit/files.py
|
|
13
|
+
src/artinkit/jsonstore.py
|
|
14
|
+
src/artinkit/logging_utils.py
|
|
15
|
+
src/artinkit/text.py
|
|
16
|
+
src/artinkit/validation.py
|
|
17
|
+
tests/test_core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
artinkit
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""ArtinKit: lightweight Python utilities with secure defaults."""
|
|
2
|
+
|
|
3
|
+
from . import files, jsonstore, text, validation
|
|
4
|
+
from .logging_utils import get_logger
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
__all__ = ["__version__", "files", "jsonstore", "text", "validation", "get_logger"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Command-line entry point for ArtinKit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import files, text
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
parser = argparse.ArgumentParser(prog="artinkit", description="Lightweight Python utilities")
|
|
13
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
14
|
+
|
|
15
|
+
normalize = subparsers.add_parser("normalize", help="collapse whitespace in text")
|
|
16
|
+
normalize.add_argument("text", help="text to normalize")
|
|
17
|
+
|
|
18
|
+
info = subparsers.add_parser("file-info", help="show basic information about a file")
|
|
19
|
+
info.add_argument("path", help="path to an existing file")
|
|
20
|
+
|
|
21
|
+
return parser
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> int:
|
|
25
|
+
args = build_parser().parse_args()
|
|
26
|
+
if args.command == "normalize":
|
|
27
|
+
print(text.normalize_whitespace(args.text))
|
|
28
|
+
return 0
|
|
29
|
+
if args.command == "file-info":
|
|
30
|
+
path = Path(args.path)
|
|
31
|
+
if not path.is_file():
|
|
32
|
+
raise SystemExit(f"Not a file: {path}")
|
|
33
|
+
print(f"Path: {path}")
|
|
34
|
+
print(f"Size: {files.file_size(path)} bytes")
|
|
35
|
+
return 0
|
|
36
|
+
return 2
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Explicit-path filesystem helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def read_text(path: str | Path, encoding: str = "utf-8") -> str:
|
|
9
|
+
"""Read a text file using UTF-8 by default."""
|
|
10
|
+
return Path(path).read_text(encoding=encoding)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def write_text(
|
|
14
|
+
path: str | Path,
|
|
15
|
+
content: str,
|
|
16
|
+
*,
|
|
17
|
+
encoding: str = "utf-8",
|
|
18
|
+
create_parents: bool = False,
|
|
19
|
+
) -> Path:
|
|
20
|
+
"""Write text to an explicit path and return the resulting Path."""
|
|
21
|
+
if not isinstance(content, str):
|
|
22
|
+
raise TypeError("content must be a string")
|
|
23
|
+
target = Path(path)
|
|
24
|
+
if create_parents:
|
|
25
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
target.write_text(content, encoding=encoding)
|
|
27
|
+
return target
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ensure_dir(path: str | Path) -> Path:
|
|
31
|
+
"""Create a directory and its parents if needed."""
|
|
32
|
+
target = Path(path)
|
|
33
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
return target
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def list_files(path: str | Path, *, recursive: bool = False) -> list[Path]:
|
|
38
|
+
"""List regular files in a directory, sorted by path."""
|
|
39
|
+
root = Path(path)
|
|
40
|
+
if not root.is_dir():
|
|
41
|
+
raise NotADirectoryError(str(root))
|
|
42
|
+
iterator = root.rglob("*") if recursive else root.iterdir()
|
|
43
|
+
return sorted((item for item in iterator if item.is_file()), key=lambda item: str(item))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def file_size(path: str | Path) -> int:
|
|
47
|
+
"""Return file size in bytes."""
|
|
48
|
+
target = Path(path)
|
|
49
|
+
if not target.is_file():
|
|
50
|
+
raise FileNotFoundError(str(target))
|
|
51
|
+
return target.stat().st_size
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Simple JSON file storage using only the standard library."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_json(path: str | Path) -> Any:
|
|
11
|
+
"""Load JSON from a UTF-8 file."""
|
|
12
|
+
with Path(path).open("r", encoding="utf-8") as stream:
|
|
13
|
+
return json.load(stream)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def save_json(
|
|
17
|
+
path: str | Path,
|
|
18
|
+
data: Any,
|
|
19
|
+
*,
|
|
20
|
+
indent: int = 2,
|
|
21
|
+
create_parents: bool = False,
|
|
22
|
+
) -> Path:
|
|
23
|
+
"""Save JSON as UTF-8, preserving non-ASCII characters."""
|
|
24
|
+
if isinstance(indent, bool) or not isinstance(indent, int) or indent < 0:
|
|
25
|
+
raise ValueError("indent must be a non-negative integer")
|
|
26
|
+
target = Path(path)
|
|
27
|
+
if create_parents:
|
|
28
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
with target.open("w", encoding="utf-8", newline="\n") as stream:
|
|
30
|
+
json.dump(data, stream, ensure_ascii=False, indent=indent, allow_nan=False)
|
|
31
|
+
stream.write("\n")
|
|
32
|
+
return target
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Logging configuration helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_logger(name: str = "artinkit", *, level: int = logging.INFO) -> logging.Logger:
|
|
9
|
+
"""Return a named logger without configuring the application's root logger."""
|
|
10
|
+
if not isinstance(name, str) or not name.strip():
|
|
11
|
+
raise ValueError("name must be a non-empty string")
|
|
12
|
+
logger = logging.getLogger(name)
|
|
13
|
+
logger.setLevel(level)
|
|
14
|
+
return logger
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Small, dependency-free text helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import unicodedata
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def normalize_whitespace(value: str) -> str:
|
|
10
|
+
"""Collapse all whitespace runs into one ASCII space and trim edges."""
|
|
11
|
+
_require_string(value, "value")
|
|
12
|
+
return " ".join(value.split())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def word_count(value: str) -> int:
|
|
16
|
+
"""Count whitespace-separated words."""
|
|
17
|
+
_require_string(value, "value")
|
|
18
|
+
return len(value.split())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def truncate(value: str, max_length: int, suffix: str = "...") -> str:
|
|
22
|
+
"""Truncate text to at most max_length characters, including the suffix."""
|
|
23
|
+
_require_string(value, "value")
|
|
24
|
+
_require_string(suffix, "suffix")
|
|
25
|
+
if isinstance(max_length, bool) or not isinstance(max_length, int):
|
|
26
|
+
raise TypeError("max_length must be an integer")
|
|
27
|
+
if max_length < 0:
|
|
28
|
+
raise ValueError("max_length must be >= 0")
|
|
29
|
+
if len(value) <= max_length:
|
|
30
|
+
return value
|
|
31
|
+
if max_length == 0:
|
|
32
|
+
return ""
|
|
33
|
+
if max_length <= len(suffix):
|
|
34
|
+
return value[:max_length]
|
|
35
|
+
return value[: max_length - len(suffix)] + suffix
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def slugify(value: str, separator: str = "-") -> str:
|
|
39
|
+
"""Create a simple lowercase ASCII slug from text."""
|
|
40
|
+
_require_string(value, "value")
|
|
41
|
+
_require_string(separator, "separator")
|
|
42
|
+
if not separator or any(ch.isalnum() for ch in separator):
|
|
43
|
+
raise ValueError("separator must be non-empty and contain no alphanumeric characters")
|
|
44
|
+
normalized = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
|
|
45
|
+
parts = re.findall(r"[a-zA-Z0-9]+", normalized.lower())
|
|
46
|
+
return separator.join(parts)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _require_string(value: str, name: str) -> None:
|
|
50
|
+
if not isinstance(value, str):
|
|
51
|
+
raise TypeError(f"{name} must be a string")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Small, predictable validation helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_non_empty_string(value: object) -> bool:
|
|
10
|
+
"""Return True when value is a string containing non-whitespace text."""
|
|
11
|
+
return isinstance(value, str) and bool(value.strip())
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_integer(value: object) -> bool:
|
|
15
|
+
"""Return True for integers but not booleans."""
|
|
16
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_finite_number(value: object) -> bool:
|
|
20
|
+
"""Return True for finite integers/floats, excluding booleans."""
|
|
21
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
22
|
+
return False
|
|
23
|
+
try:
|
|
24
|
+
return math.isfinite(value)
|
|
25
|
+
except OverflowError:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def matches_pattern(value: str, pattern: str) -> bool:
|
|
30
|
+
"""Return whether an entire string matches a regular expression."""
|
|
31
|
+
if not isinstance(value, str) or not isinstance(pattern, str):
|
|
32
|
+
raise TypeError("value and pattern must be strings")
|
|
33
|
+
return re.fullmatch(pattern, value) is not None
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from artinkit import files, jsonstore, text, validation
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_normalize_whitespace_and_word_count():
|
|
9
|
+
assert text.normalize_whitespace(" hello\t world \n") == "hello world"
|
|
10
|
+
assert text.word_count("one two three") == 3
|
|
11
|
+
assert text.word_count(" ") == 0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_slugify():
|
|
15
|
+
assert text.slugify("ArtinKit Utilities!") == "artinkit-utilities"
|
|
16
|
+
assert text.slugify("A B", separator="_") == "a_b"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_truncate_respects_limit():
|
|
20
|
+
assert text.truncate("abcdef", 4) == "a..."
|
|
21
|
+
assert text.truncate("abcdef", 2) == "ab"
|
|
22
|
+
assert text.truncate("abc", 4) == "abc"
|
|
23
|
+
assert text.truncate("abc", 0) == ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_text_validation():
|
|
27
|
+
with pytest.raises(TypeError):
|
|
28
|
+
text.normalize_whitespace(42) # type: ignore[arg-type]
|
|
29
|
+
with pytest.raises(ValueError):
|
|
30
|
+
text.truncate("abc", -1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_write_read_and_create_parent(tmp_path):
|
|
34
|
+
target = files.write_text(tmp_path / "nested" / "hello.txt", "سلام", create_parents=True)
|
|
35
|
+
assert files.read_text(target) == "سلام"
|
|
36
|
+
assert files.file_size(target) > 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_list_files_and_ensure_dir(tmp_path):
|
|
40
|
+
folder = files.ensure_dir(tmp_path / "data")
|
|
41
|
+
(folder / "a.txt").write_text("a", encoding="utf-8")
|
|
42
|
+
(folder / "b.txt").write_text("b", encoding="utf-8")
|
|
43
|
+
(folder / "nested").mkdir()
|
|
44
|
+
(folder / "nested" / "c.txt").write_text("c", encoding="utf-8")
|
|
45
|
+
assert [p.name for p in files.list_files(folder)] == ["a.txt", "b.txt"]
|
|
46
|
+
assert len(files.list_files(folder, recursive=True)) == 3
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_json_roundtrip_preserves_unicode(tmp_path):
|
|
50
|
+
target = tmp_path / "config.json"
|
|
51
|
+
data = {"language": "فارسی", "enabled": True, "items": [1, 2]}
|
|
52
|
+
jsonstore.save_json(target, data)
|
|
53
|
+
assert jsonstore.load_json(target) == data
|
|
54
|
+
assert "فارسی" in target.read_text(encoding="utf-8")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_json_rejects_nan(tmp_path):
|
|
58
|
+
with pytest.raises(ValueError):
|
|
59
|
+
jsonstore.save_json(tmp_path / "bad.json", {"x": float("nan")})
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_json_indent_validation(tmp_path):
|
|
63
|
+
with pytest.raises(ValueError):
|
|
64
|
+
jsonstore.save_json(tmp_path / "bad.json", {}, indent=-1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_validation_helpers():
|
|
68
|
+
assert validation.is_non_empty_string(" hi ")
|
|
69
|
+
assert not validation.is_non_empty_string(" ")
|
|
70
|
+
assert validation.is_integer(2)
|
|
71
|
+
assert not validation.is_integer(True)
|
|
72
|
+
assert validation.is_finite_number(1.5)
|
|
73
|
+
assert not validation.is_finite_number(float("inf"))
|
|
74
|
+
assert validation.matches_pattern("ABC-123", r"[A-Z]{3}-\d{3}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_cli_normalize(capsys, monkeypatch):
|
|
78
|
+
from artinkit import cli
|
|
79
|
+
monkeypatch.setattr("sys.argv", ["artinkit", "normalize", " hello world "])
|
|
80
|
+
assert cli.main() == 0
|
|
81
|
+
assert capsys.readouterr().out.strip() == "hello world"
|