charded 0.1.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.
- charded/__init__.py +3 -0
- charded/string.py +272 -0
- charded-0.1.0.dist-info/METADATA +125 -0
- charded-0.1.0.dist-info/RECORD +6 -0
- charded-0.1.0.dist-info/WHEEL +5 -0
- charded-0.1.0.dist-info/top_level.txt +1 -0
charded/__init__.py
ADDED
charded/string.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from math import log2
|
|
4
|
+
from re import findall
|
|
5
|
+
|
|
6
|
+
from kain.descriptors import pin, proxy_to
|
|
7
|
+
from kain.importer import optional
|
|
8
|
+
from kain.internals import Who, to_ascii, unique
|
|
9
|
+
|
|
10
|
+
logger = getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def generate_binary_offsets(x: int):
|
|
14
|
+
power = int(log2(x))
|
|
15
|
+
|
|
16
|
+
yield 0
|
|
17
|
+
for base in range(power):
|
|
18
|
+
bias = (2 ** (power - base))
|
|
19
|
+
for no in range(x // bias + 1):
|
|
20
|
+
if no % 2:
|
|
21
|
+
yield no * bias
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def iter_by_binary_offsets(x: bytes | str, /, ordinate: bool):
|
|
25
|
+
getter = x.__getitem__
|
|
26
|
+
offset = len(x) - len(x) % 2 - 1
|
|
27
|
+
|
|
28
|
+
if offset != -1:
|
|
29
|
+
iterator = generate_binary_offsets(offset)
|
|
30
|
+
|
|
31
|
+
offsets = map(getter, iterator)
|
|
32
|
+
if ordinate:
|
|
33
|
+
offsets = map(ord, offsets)
|
|
34
|
+
yield from enumerate(offsets)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def replace_class_with_str(method, *args, **kw):
|
|
38
|
+
if args and isinstance(args[0], Str):
|
|
39
|
+
args = (str(args[0]), *args[1:])
|
|
40
|
+
return method(*args, **kw)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class External:
|
|
44
|
+
|
|
45
|
+
@pin.cls
|
|
46
|
+
def _charset_detect(cls):
|
|
47
|
+
return optional("charset_normalizer.detect")
|
|
48
|
+
|
|
49
|
+
@pin.cls
|
|
50
|
+
def _from_buffer(cls):
|
|
51
|
+
return optional("magic.from_buffer")
|
|
52
|
+
|
|
53
|
+
@pin.cls
|
|
54
|
+
def _from_content(cls):
|
|
55
|
+
return optional("magic.detect_from_content")
|
|
56
|
+
|
|
57
|
+
#
|
|
58
|
+
|
|
59
|
+
@pin.cls
|
|
60
|
+
def charset_detect(cls):
|
|
61
|
+
if detect := External._charset_detect:
|
|
62
|
+
def charset_detect(x: bytes) -> str:
|
|
63
|
+
return detect(x)
|
|
64
|
+
else:
|
|
65
|
+
def charset_detect(x: bytes) -> None: ...
|
|
66
|
+
|
|
67
|
+
return charset_detect
|
|
68
|
+
|
|
69
|
+
@pin.cls
|
|
70
|
+
def mime_reader(cls):
|
|
71
|
+
if (read := cls._from_buffer) and (detect := cls._from_content):
|
|
72
|
+
def mime_reader(x: bytes) -> dict[str, str]:
|
|
73
|
+
return {
|
|
74
|
+
"type": detect(x).mime_type,
|
|
75
|
+
"description": read(x)}
|
|
76
|
+
else:
|
|
77
|
+
def mime_reader(x: bytes) -> None: ...
|
|
78
|
+
|
|
79
|
+
return mime_reader
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@proxy_to(
|
|
83
|
+
"text",
|
|
84
|
+
"__add__", "__contains__", "__eq__", "__format__", "__hash__", "__ge__",
|
|
85
|
+
"__getitem__", "__gt__", "__iter__", "__le__", "__len__", "__lt__", "__mod__",
|
|
86
|
+
"__mul__", "__ne__", "__rmod__", "__rmul__", "__sizeof__", "capitalize",
|
|
87
|
+
"casefold", "center", "count", "encode", "endswith", "expandtabs", "find",
|
|
88
|
+
"format", "format_map", "index", "isalnum", "isalpha", "isascii", "isdecimal",
|
|
89
|
+
"isdigit", "isidentifier", "islower", "isnumeric", "isprintable", "isspace",
|
|
90
|
+
"istitle", "isupper", "join", "ljust", "lower", "lstrip", "maketrans",
|
|
91
|
+
"partition", "removeprefix", "removesuffix", "replace", "rfind", "rindex",
|
|
92
|
+
"rjust", "rpartition", "rsplit", "rstrip", "split", "splitlines", "startswith",
|
|
93
|
+
"strip", "swapcase", "title", "translate", "upper", "zfill",
|
|
94
|
+
pin, pre=replace_class_with_str,
|
|
95
|
+
)
|
|
96
|
+
class Str:
|
|
97
|
+
"""A class for handling bytes | str objects.
|
|
98
|
+
|
|
99
|
+
Autodetect charsets, autoencode/decode from bytes/text.
|
|
100
|
+
Just pass any bytes | str object to Str and use it:
|
|
101
|
+
|
|
102
|
+
x = Str(b'hello')
|
|
103
|
+
str(x) == 'hello'
|
|
104
|
+
bytes(x) == b'hello'
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
InternalCharset : str = "utf-8"
|
|
109
|
+
DetectionSizeLimit: int = 2 ** 20
|
|
110
|
+
DetectionScanLimit: int = 2 ** 10
|
|
111
|
+
|
|
112
|
+
to_ascii = staticmethod(to_ascii)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def to_bytes(cls, obj, *args, **kw):
|
|
116
|
+
return bytes(cls(obj, *args, **kw))
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def to_text(cls, obj, *args, **kw):
|
|
120
|
+
return str(cls(obj, *args, **kw))
|
|
121
|
+
|
|
122
|
+
#
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def downcast(cls, obj):
|
|
126
|
+
if isinstance(obj, bytes | str):
|
|
127
|
+
return obj
|
|
128
|
+
|
|
129
|
+
if isinstance(obj, int):
|
|
130
|
+
return str(obj)
|
|
131
|
+
|
|
132
|
+
msg = f"{Who(cls)} accept bytes | str, got {Who.Is(obj)}"
|
|
133
|
+
raise TypeError(msg)
|
|
134
|
+
|
|
135
|
+
@pin
|
|
136
|
+
def mime(self):
|
|
137
|
+
try:
|
|
138
|
+
return External.mime_reader(self.bytes)
|
|
139
|
+
|
|
140
|
+
except Exception: # noqa: BLE001 Do not catch blind exception: `Exception`
|
|
141
|
+
logger.warning(
|
|
142
|
+
f"something went wrong for {self.bytes[:2**10]!r}", exc_info=True)
|
|
143
|
+
|
|
144
|
+
#
|
|
145
|
+
|
|
146
|
+
def __bytes__(self):
|
|
147
|
+
return self.bytes
|
|
148
|
+
|
|
149
|
+
def __str__(self):
|
|
150
|
+
return self.text
|
|
151
|
+
|
|
152
|
+
def __init__(self, obj, /, charset=None):
|
|
153
|
+
self._object = obj
|
|
154
|
+
self._charset = charset or self.InternalCharset
|
|
155
|
+
|
|
156
|
+
@pin
|
|
157
|
+
def _value(self):
|
|
158
|
+
return self.downcast(self._object)
|
|
159
|
+
|
|
160
|
+
#
|
|
161
|
+
|
|
162
|
+
@pin
|
|
163
|
+
def external_charset(self):
|
|
164
|
+
return External.charset_detect(self.bytes)
|
|
165
|
+
|
|
166
|
+
@pin
|
|
167
|
+
def probe_order(self):
|
|
168
|
+
result = [self._charset]
|
|
169
|
+
|
|
170
|
+
if meta := self.external_charset:
|
|
171
|
+
result.append(meta["encoding"])
|
|
172
|
+
|
|
173
|
+
return tuple(unique([*result, "ascii"]))
|
|
174
|
+
|
|
175
|
+
@pin
|
|
176
|
+
def compatible_charset(self):
|
|
177
|
+
string = self._value
|
|
178
|
+
|
|
179
|
+
if isinstance(string, bytes):
|
|
180
|
+
method = string.decode
|
|
181
|
+
order = self.probe_order
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
method = string.encode
|
|
185
|
+
order = tuple(reversed(self.probe_order))
|
|
186
|
+
|
|
187
|
+
for charset in filter(bool, order):
|
|
188
|
+
with suppress(UnicodeEncodeError, UnicodeDecodeError):
|
|
189
|
+
method(charset)
|
|
190
|
+
return charset
|
|
191
|
+
|
|
192
|
+
@pin
|
|
193
|
+
def charset(self):
|
|
194
|
+
string = self._value
|
|
195
|
+
is_bytes = isinstance(string, bytes)
|
|
196
|
+
|
|
197
|
+
read_limit = self.DetectionSizeLimit
|
|
198
|
+
if not is_bytes and len(string) <= read_limit:
|
|
199
|
+
read_limit = 0
|
|
200
|
+
|
|
201
|
+
default = "ascii"
|
|
202
|
+
if not string:
|
|
203
|
+
return default
|
|
204
|
+
|
|
205
|
+
collected = set()
|
|
206
|
+
scan_limit = self.DetectionScanLimit
|
|
207
|
+
|
|
208
|
+
for no, char in iter_by_binary_offsets(string, ordinate=not is_bytes):
|
|
209
|
+
if no > read_limit:
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
if char < 0x20: # noqa: PLR2004
|
|
213
|
+
if char in (0x9, 0xa, 0xc, 0xd):
|
|
214
|
+
continue
|
|
215
|
+
return "binary"
|
|
216
|
+
|
|
217
|
+
if char >= 0x7f: # noqa: PLR2004, ANSI Extended Border
|
|
218
|
+
if is_bytes:
|
|
219
|
+
default = "ansi"
|
|
220
|
+
|
|
221
|
+
elif char not in collected:
|
|
222
|
+
if len(collected) >= scan_limit:
|
|
223
|
+
return self.compatible_charset
|
|
224
|
+
collected.add(char)
|
|
225
|
+
|
|
226
|
+
return default if is_bytes else self.compatible_charset
|
|
227
|
+
|
|
228
|
+
#
|
|
229
|
+
|
|
230
|
+
@pin
|
|
231
|
+
def bytes(self):
|
|
232
|
+
string = self._value
|
|
233
|
+
return string if isinstance(string, bytes) else string.encode(self._charset)
|
|
234
|
+
|
|
235
|
+
@pin
|
|
236
|
+
def text(self):
|
|
237
|
+
string = self._value
|
|
238
|
+
if isinstance(string, str):
|
|
239
|
+
return string
|
|
240
|
+
|
|
241
|
+
charset = self.charset
|
|
242
|
+
if charset == "ansi":
|
|
243
|
+
charset = self.compatible_charset
|
|
244
|
+
|
|
245
|
+
if charset != "binary":
|
|
246
|
+
return string.decode(charset)
|
|
247
|
+
|
|
248
|
+
msg = f"couldn't {charset=} decode {string[:2**10]!r}"
|
|
249
|
+
if charset := self.charset:
|
|
250
|
+
msg = f"{msg}; {charset}"
|
|
251
|
+
|
|
252
|
+
raise ValueError(msg)
|
|
253
|
+
|
|
254
|
+
def __repr__(self):
|
|
255
|
+
string = self._value
|
|
256
|
+
size = f"{len(self.bytes):d}"
|
|
257
|
+
|
|
258
|
+
length = ""
|
|
259
|
+
charset = (f"{self.charset} ".upper()) if self.charset else ""
|
|
260
|
+
|
|
261
|
+
if isinstance(string, str) and len(self.bytes) != len(self.text):
|
|
262
|
+
length = f"={len(self.text):d}"
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
f"<{charset}{Who(self, full=False)}"
|
|
266
|
+
f"[{Who(self._object, full=False)}"
|
|
267
|
+
f"({size})]{length} at {id(self):#x}>")
|
|
268
|
+
|
|
269
|
+
#
|
|
270
|
+
|
|
271
|
+
def tokenize(self, regex=r"([\w\d]+)"):
|
|
272
|
+
return findall(regex, self.text)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: charded
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: charded
|
|
5
|
+
Author-email: Alex Kalaverin <alex@kalaver.in>
|
|
6
|
+
Project-URL: Homepage, https://kalaver.in
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Requires-Python: <3.13,>=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
Requires-Dist: charset-normalizer>=3.4.2
|
|
18
|
+
Requires-Dist: kain<0.2.0,>=0.1.3
|
|
19
|
+
Requires-Dist: python-magic>=0.4.27
|
|
20
|
+
|
|
21
|
+
# Simple project for Python 3.10+
|
|
22
|
+
#### All-in-one repository with supervisor, crontab, linters and many useful tools
|
|
23
|
+
|
|
24
|
+
**Version control** is handled using [Astral UV](https://docs.astral.sh/uv/getting-started/installation/#standalone-installer) tool. When building the image, uv is sourced from the official repository by copying the binary. Installation on a developer's machine can be done in various ways, which we'll cover shortly.
|
|
25
|
+
|
|
26
|
+
**Managing the interpreter version**, project environment variables, and setting up the virtual environment is done with the [Mise](https://mise.jdx.dev/installing-mise.html) tool. It automatically install any interpreter version by reading it from the project description and/or the version bound by uv. It can also fetch the appropriate uv binary for the platform and architecture.
|
|
27
|
+
|
|
28
|
+
## How to install required tools?
|
|
29
|
+
|
|
30
|
+
#### A. Quick start for test
|
|
31
|
+
|
|
32
|
+
Therefore, the quickest and most minimal way to get touch is to install mise on your system and prepare tools with mise, **not for development**:
|
|
33
|
+
|
|
34
|
+
1. `brew install mise`
|
|
35
|
+
|
|
36
|
+
That's all, go to **Shell configuration** section.
|
|
37
|
+
|
|
38
|
+
#### B. Engineer full-featured setup
|
|
39
|
+
|
|
40
|
+
**True engineer way** it's prepare rust environment, build mise and configure shell:
|
|
41
|
+
|
|
42
|
+
1. Install Cargo via [Rustup](https://doc.rust-lang.org/book/ch01-01-installation.html).
|
|
43
|
+
2. Do not forget add cargo path for your shell:
|
|
44
|
+
- `export PATH="~/.cargo/bin:$PATH"`
|
|
45
|
+
3. Install sccache to avoid electricity bills:
|
|
46
|
+
- `cargo install sccache`
|
|
47
|
+
4. Activate sccache:
|
|
48
|
+
- `export RUSTC_WRAPPER="~/.cargo/bin/sccache"` (and add it to your shell)
|
|
49
|
+
5. Install cargo packages updater and mise:
|
|
50
|
+
- `cargo install cargo-update mise`
|
|
51
|
+
6. Install uv:
|
|
52
|
+
- `mise install uv@latest && mise use -g uv@latest`
|
|
53
|
+
7. That's all, you have last version of optimized tools; for update all packages just run sometime:
|
|
54
|
+
- `rustup update && cargo install-update --all`
|
|
55
|
+
|
|
56
|
+
### Shell configuration
|
|
57
|
+
|
|
58
|
+
1. **Mise provide dotenv functionality** (automatic read per-project environment variables from .env file and from .mise.toml config) from the box with batteries, but your shell must have entry hook, add to your shell it, example for zsh (your can replace it for bash, fish, etc):
|
|
59
|
+
- `eval "$(mise activate zsh)"`
|
|
60
|
+
2. Also you can want using autocompletion, same story for zsh:
|
|
61
|
+
- `eval "$(mise completion zsh && uv generate-shell-completion zsh)"`
|
|
62
|
+
3. Restart your shell session:
|
|
63
|
+
- `exec "$SHELL"`
|
|
64
|
+
|
|
65
|
+
### Kickstart
|
|
66
|
+
|
|
67
|
+
1. Go to project root.
|
|
68
|
+
2. Just run `make`:
|
|
69
|
+
- mise will mark project directory as trusted
|
|
70
|
+
- mise copy sample development environment variables to .env
|
|
71
|
+
- mise grab environment variables defined in project .env, evaluate it and provide to current shell session
|
|
72
|
+
- mise checks what project python versions is installed, otherwise download and install it
|
|
73
|
+
- uv make virtual environment in project root (`uv venv`)
|
|
74
|
+
- uv read project packages list, download, install and link it (via `uv sync` run, read Makefile)
|
|
75
|
+
- uv install pre-commit and pre-push hooks
|
|
76
|
+
|
|
77
|
+
## Work with project
|
|
78
|
+
|
|
79
|
+
### Warning about pip
|
|
80
|
+
|
|
81
|
+
**NEVER CALL pip, NEVER!** Instead it use native uv calls, [read uv manual](https://docs.astral.sh/uv/guides/projects/#managing-dependencies), it's very easy, for example:
|
|
82
|
+
|
|
83
|
+
1. Set or change python version (when python 3.11 already installed), before run do not forget change your python version in pyproject.toml:
|
|
84
|
+
- `uv python pin 3.11 && make sync`
|
|
85
|
+
|
|
86
|
+
2. If python 3.11 isn't installed, run `mise install python@3.11` and mise download and install python 3.11, recreate virtual environment with 3.11 context. Do not forget to pin python version by uv from previous step (and, may be you need to update your pyproject.toml).
|
|
87
|
+
|
|
88
|
+
2. Just add new dependency:
|
|
89
|
+
- `uv add phpbb<=1.2`
|
|
90
|
+
|
|
91
|
+
3. Add some development library:
|
|
92
|
+
- `uv add --group development backyard-memleak`
|
|
93
|
+
|
|
94
|
+
4. Work with locally cloned repository:
|
|
95
|
+
- `uv add --editable ~/src/lib/chrome-v8-core`
|
|
96
|
+
|
|
97
|
+
### Common workflow
|
|
98
|
+
|
|
99
|
+
1. `make`:
|
|
100
|
+
- same as `mise install`, but also call `mise trust --yes` for initial deployment
|
|
101
|
+
- call `make sync`
|
|
102
|
+
|
|
103
|
+
2. `make sync`
|
|
104
|
+
- drop and recreate .venv by `uv venv`
|
|
105
|
+
- read project dependencies graph from pyproject.toml and install it to virtual environment by `uv sync`)
|
|
106
|
+
- call `make freeze`
|
|
107
|
+
|
|
108
|
+
3. `make freeze`:
|
|
109
|
+
- dump state to uv.lock by `uv lock`
|
|
110
|
+
- for development and debugging puproses uv save all used packages in current virtual environment to `packages.json` (with all development packages!) by `uv pip list`
|
|
111
|
+
- for repeatable production purposes uv save project dependencies to `packages.txt` with hashes for release builds strict version checks, read Dockerfile example (only project dependencies!) by `uv pip compile`
|
|
112
|
+
|
|
113
|
+
4. `make upgrade`:
|
|
114
|
+
- read project dependencies graph from pyproject.toml
|
|
115
|
+
- fetch information about all updated packages, recreate dependencies graph and install it to virtual environment by `uv sync --upgrade`
|
|
116
|
+
- update `uv.lock` with updated packages version by `uv lock --upgrade`
|
|
117
|
+
- call `make freeze`
|
|
118
|
+
- show all installed packages in local virtual environment
|
|
119
|
+
- all you need it's just manually update versions in pyproject.toml
|
|
120
|
+
|
|
121
|
+
5. `make check`:
|
|
122
|
+
It's non-destructive action, just run all checks and stop at first fail.
|
|
123
|
+
|
|
124
|
+
6. `make lint`:
|
|
125
|
+
**Destructive action**, always commit all changes before run it. Runs all compatible linters with --fix and --edit mode, after it call `make check` for final polishing.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
charded/__init__.py,sha256=BAPU0Z8j9BS4AyJ4fuk4HDDuv-V08XTlZjRMqE0Tcn0,51
|
|
2
|
+
charded/string.py,sha256=dAeBm9ietA1qjwuYV0-F6FSaZA2oSvLWMvow7CkfLkM,7330
|
|
3
|
+
charded-0.1.0.dist-info/METADATA,sha256=J0hzWvs6uxg7N1moqigl93V20K2qPDdpaIaT1G2xJPw,6138
|
|
4
|
+
charded-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
charded-0.1.0.dist-info/top_level.txt,sha256=dp0lwnMr-wQOUmTRVMLXZEHKLq-7gawcmB8VE--TTQE,8
|
|
6
|
+
charded-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
charded
|