sltcore 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.
- sltcore-0.1.0/.github/FUNDING.yml +3 -0
- sltcore-0.1.0/.github/workflows/publish_to_pypi.yml +25 -0
- sltcore-0.1.0/.github/workflows/publish_to_testpypi.yml +26 -0
- sltcore-0.1.0/.gitignore +218 -0
- sltcore-0.1.0/.python-version +1 -0
- sltcore-0.1.0/.vscode/launch.json +23 -0
- sltcore-0.1.0/.vscode/settings.json +3 -0
- sltcore-0.1.0/LICENSE +21 -0
- sltcore-0.1.0/PKG-INFO +12 -0
- sltcore-0.1.0/README.md +3 -0
- sltcore-0.1.0/pyproject.toml +32 -0
- sltcore-0.1.0/src/sltcore/__init__.py +0 -0
- sltcore-0.1.0/src/sltcore/bits.py +88 -0
- sltcore-0.1.0/src/sltcore/types.py +158 -0
- sltcore-0.1.0/tests/test_bits.py +87 -0
- sltcore-0.1.0/tests/test_types.py +108 -0
- sltcore-0.1.0/uv.lock +79 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
release:
|
|
6
|
+
types: [published]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-and-publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v3
|
|
17
|
+
|
|
18
|
+
- name: Build wheel
|
|
19
|
+
run: uv build
|
|
20
|
+
|
|
21
|
+
- name: Publish to PyPI
|
|
22
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
23
|
+
with:
|
|
24
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
25
|
+
packages_dir: dist
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Publish to TestPyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
release:
|
|
6
|
+
types: [published]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-and-publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v3
|
|
17
|
+
|
|
18
|
+
- name: Build wheel
|
|
19
|
+
run: uv build
|
|
20
|
+
|
|
21
|
+
- name: Publish to TestPyPI
|
|
22
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
23
|
+
with:
|
|
24
|
+
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
|
25
|
+
repository-url: https://test.pypi.org/legacy/
|
|
26
|
+
packages_dir: dist
|
sltcore-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2.0",
|
|
3
|
+
"configurations": [
|
|
4
|
+
{
|
|
5
|
+
"name": "Debug Pytest (single test)",
|
|
6
|
+
"type": "python",
|
|
7
|
+
"request": "launch",
|
|
8
|
+
"module": "pytest",
|
|
9
|
+
"args": [
|
|
10
|
+
"-k", "${input:test_name}"
|
|
11
|
+
],
|
|
12
|
+
"console": "integratedTerminal"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"inputs": [
|
|
16
|
+
{
|
|
17
|
+
"id": "test_name",
|
|
18
|
+
"type": "promptString",
|
|
19
|
+
"description": "Enter pytest -k expression",
|
|
20
|
+
"default": "test_bits_get_cross_byte"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
sltcore-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fangface
|
|
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.
|
sltcore-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sltcore
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A toolkit for handling structured data layouts and bit-level operations.
|
|
5
|
+
Author: fangface
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# StructLayoutToolkitCore
|
|
11
|
+
|
|
12
|
+
stlcore is a minimal, high‑cohesion core library for bit‑accurate binary slicing. It provides the fundamental primitives (bits_get, bits_set, InfoSize) required to build struct layout analyzers, binary editors, protocol inspectors, and low‑level testing tools. Designed for clarity, correctness, and composability.
|
sltcore-0.1.0/README.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# StructLayoutToolkitCore
|
|
2
|
+
|
|
3
|
+
stlcore is a minimal, high‑cohesion core library for bit‑accurate binary slicing. It provides the fundamental primitives (bits_get, bits_set, InfoSize) required to build struct layout analyzers, binary editors, protocol inspectors, and low‑level testing tools. Designed for clarity, correctness, and composability.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sltcore"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A toolkit for handling structured data layouts and bit-level operations."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "fangface" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["hatchling"]
|
|
14
|
+
build-backend = "hatchling.build"
|
|
15
|
+
|
|
16
|
+
[tool.hatch.build.targets.wheel]
|
|
17
|
+
packages = ["sltcore"]
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=9.1.1",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
pythonpath = ["src"]
|
|
26
|
+
testpaths = ["tests"]
|
|
27
|
+
|
|
28
|
+
[[tool.uv.index]]
|
|
29
|
+
name = "testpypi"
|
|
30
|
+
url = "https://test.pypi.org/simple/"
|
|
31
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
32
|
+
explicit = true
|
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Bit-level operations for structured data layouts."""
|
|
2
|
+
from sltcore.types import Info, InfoSize
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def bits_get(buf: bytearray, offset: InfoSize, size: InfoSize) -> Info:
|
|
6
|
+
""" Extracts a slice of bits from the given bytearray
|
|
7
|
+
starting at the specified bit offset and spanning
|
|
8
|
+
the specified bit size. Returns an Info object containing
|
|
9
|
+
the extracted raw value and an InfoSize representing
|
|
10
|
+
the size of the extracted data.
|
|
11
|
+
Parameters:
|
|
12
|
+
- buf (bytearray): The source bytearray to extract bits from.
|
|
13
|
+
- offset (InfoSize): The bit offset from the start of buf to begin
|
|
14
|
+
extraction.
|
|
15
|
+
- size (InfoSize): The number of bits to extract.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
- Info: An Info object where raw_value contains the extracted bits as an
|
|
19
|
+
integer, and info_size indicates the size of the extracted data.
|
|
20
|
+
|
|
21
|
+
Notes:
|
|
22
|
+
- The extraction returns the bits as an integer in the raw_value
|
|
23
|
+
field of the Info object.
|
|
24
|
+
- Only the necessary byte range is extracted.
|
|
25
|
+
- Leading and trailing unwanted bits are masked out.
|
|
26
|
+
- The returned Info object's raw_value is front-packed, meaning
|
|
27
|
+
the first bit of the extracted range aligns with the most
|
|
28
|
+
significant bit of raw_value.
|
|
29
|
+
"""
|
|
30
|
+
# Correct byte span (handles cross-byte bit ranges)
|
|
31
|
+
need = _required_bytes_for_extraction(offset, size)
|
|
32
|
+
|
|
33
|
+
# Extract bytes
|
|
34
|
+
chunk = buf[offset.byte:offset.byte + need]
|
|
35
|
+
value = int.from_bytes(chunk, "big")
|
|
36
|
+
|
|
37
|
+
# Front-pack
|
|
38
|
+
shift = (need * 8) - size.bits - offset.bit
|
|
39
|
+
value >>= shift
|
|
40
|
+
|
|
41
|
+
# Mask to exact bit length
|
|
42
|
+
value &= (1 << size.bits) - 1
|
|
43
|
+
|
|
44
|
+
return Info(raw_value=value, info_size=size)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def bits_set(buf: bytearray, offset: InfoSize, size: InfoSize,
|
|
48
|
+
value: int) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Set a bit slice in `buf` starting at `offset` and spanning `size`.
|
|
51
|
+
`value` is an integer whose lower `size.bits` bits are written.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# Correct byte span
|
|
55
|
+
need = _required_bytes_for_extraction(offset, size)
|
|
56
|
+
|
|
57
|
+
start = offset.byte
|
|
58
|
+
end = start + need
|
|
59
|
+
|
|
60
|
+
# Read existing bytes
|
|
61
|
+
chunk = int.from_bytes(buf[start:end], "big")
|
|
62
|
+
|
|
63
|
+
# Compute shift
|
|
64
|
+
shift = (need * 8) - size.bits - offset.bit
|
|
65
|
+
|
|
66
|
+
# Build mask
|
|
67
|
+
mask = ((1 << size.bits) - 1) << shift
|
|
68
|
+
|
|
69
|
+
# Clear target field and insert new value
|
|
70
|
+
chunk = (chunk & ~mask) | ((value << shift) & mask)
|
|
71
|
+
|
|
72
|
+
# Write back
|
|
73
|
+
buf[start:end] = chunk.to_bytes(need, "big")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _required_bytes_for_extraction(offset: InfoSize, size: InfoSize) -> int:
|
|
77
|
+
"""
|
|
78
|
+
Calculates the number of bytes required to extract a bit slice
|
|
79
|
+
starting at the given offset and spanning the given size.
|
|
80
|
+
|
|
81
|
+
Parameters:
|
|
82
|
+
- offset (InfoSize): The bit offset from the start of the buffer.
|
|
83
|
+
- size (InfoSize): The number of bits to extract.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
- int: The number of bytes required to extract the specified bit slice.
|
|
87
|
+
"""
|
|
88
|
+
return (offset.bit + size.bits + 7) // 8
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Type definitions for structured data layouts and bit-level operations."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class InfoSize:
|
|
7
|
+
"""Represents a size in bytes and bits."""
|
|
8
|
+
byte: int = 0
|
|
9
|
+
bit: int = 0
|
|
10
|
+
|
|
11
|
+
def __post_init__(self):
|
|
12
|
+
"""Ensures that the bit count is normalized to be less than 8,
|
|
13
|
+
and adjusts the byte count accordingly."""
|
|
14
|
+
extra_bytes, new_bits = divmod(self.bit, 8)
|
|
15
|
+
object.__setattr__(self, "byte", self.byte + extra_bytes)
|
|
16
|
+
object.__setattr__(self, "bit", new_bits)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def bits(self) -> int:
|
|
20
|
+
"""Returns the total size in bits."""
|
|
21
|
+
return self.byte * 8 + self.bit
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def bytes(self) -> int:
|
|
25
|
+
"""Returns the total size in bytes,
|
|
26
|
+
rounding up if there are leftover bits."""
|
|
27
|
+
return (self.bits + 7) // 8 # round up
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def hex_digits(self) -> int:
|
|
31
|
+
"""Returns the number of hexadecimal digits
|
|
32
|
+
needed to represent the size in bits."""
|
|
33
|
+
return self.bits // 4
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def mask(self) -> int:
|
|
37
|
+
"""Returns a bitmask corresponding to the size in bits."""
|
|
38
|
+
return (1 << self.bits) - 1
|
|
39
|
+
|
|
40
|
+
def __add__(self, other: "InfoSize") -> "InfoSize":
|
|
41
|
+
""" Adds two InfoSize instances together, returning a new InfoSize
|
|
42
|
+
with the combined byte and bit counts, normalized to ensure
|
|
43
|
+
the bit count is less than 8."""
|
|
44
|
+
return InfoSize(self.byte + other.byte, self.bit + other.bit)
|
|
45
|
+
|
|
46
|
+
def __sub__(self, other: "InfoSize") -> "InfoSize":
|
|
47
|
+
""" Subtracts one InfoSize from another, returning a new InfoSize
|
|
48
|
+
with the resulting byte and bit counts, normalized to ensure
|
|
49
|
+
the bit count is less than 8. Raises ValueError if the result
|
|
50
|
+
would be negative."""
|
|
51
|
+
total_bits = self.bits - other.bits
|
|
52
|
+
if total_bits < 0:
|
|
53
|
+
raise ValueError("InfoSize subtraction resulted in negative size")
|
|
54
|
+
return InfoSize(0, total_bits)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class Info:
|
|
59
|
+
"""Represents a slice of bits extracted from a bytearray,
|
|
60
|
+
along with its size information."""
|
|
61
|
+
raw_value: int
|
|
62
|
+
info_size: InfoSize
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def to_unsigned_int(self) -> int:
|
|
66
|
+
"""Returns the extracted bits as an unsigned integer."""
|
|
67
|
+
return self.raw_value
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def to_signed_int(self) -> int:
|
|
71
|
+
"""Returns the extracted bits as a signed integer."""
|
|
72
|
+
raw = self.to_unsigned_int
|
|
73
|
+
bits = self.info_size.bits
|
|
74
|
+
sign_bit = 1 << (bits - 1)
|
|
75
|
+
return (raw ^ sign_bit) - sign_bit
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def to_hex(self) -> str:
|
|
79
|
+
"""Returns the extracted bits as a hexadecimal string."""
|
|
80
|
+
return hex(self.to_unsigned_int)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def to_bytes(self) -> bytes:
|
|
84
|
+
"""Returns the extracted bits as a bytes object."""
|
|
85
|
+
value = self.to_unsigned_int
|
|
86
|
+
byte_len = self.info_size.bytes
|
|
87
|
+
return value.to_bytes(byte_len, byteorder="big")
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def to_bool(self) -> bool:
|
|
91
|
+
"""Returns the extracted bits as a boolean value."""
|
|
92
|
+
return bool(self.to_unsigned_int)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def to_float(self) -> float:
|
|
96
|
+
"""Returns the extracted bits as a float."""
|
|
97
|
+
raw_int = self.to_unsigned_int
|
|
98
|
+
return _float_from_bits(raw_int, self.info_size.bits)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _float_from_bits(raw: int, bits: int) -> float:
|
|
102
|
+
"""Converts a raw integer representation of a float
|
|
103
|
+
to a Python float, based on the specified bit size.
|
|
104
|
+
Supports 16, 32, and 64 bits.
|
|
105
|
+
"""
|
|
106
|
+
if bits == 16:
|
|
107
|
+
"""IEEE754 half-precision (16bit) → float32"""
|
|
108
|
+
s = (raw >> 15) & 0x0001
|
|
109
|
+
e = (raw >> 10) & 0x001F
|
|
110
|
+
f = raw & 0x03FF
|
|
111
|
+
|
|
112
|
+
if e == 0:
|
|
113
|
+
if f == 0:
|
|
114
|
+
return (-1)**s * 0.0
|
|
115
|
+
return (-1)**s * (f / 2**10) * 2**(-14)
|
|
116
|
+
elif e == 31:
|
|
117
|
+
if f == 0:
|
|
118
|
+
return float('inf') if s == 0 else float('-inf')
|
|
119
|
+
return float('nan')
|
|
120
|
+
else:
|
|
121
|
+
return (-1)**s * (1 + f / 2**10) * 2**(e - 15)
|
|
122
|
+
elif bits == 32:
|
|
123
|
+
# IEEE754 single precision
|
|
124
|
+
# High-speed version without using struct
|
|
125
|
+
s = (raw >> 31) & 0x1
|
|
126
|
+
|
|
127
|
+
e = (raw >> 23) & 0xFF
|
|
128
|
+
f = raw & 0x7FFFFF
|
|
129
|
+
|
|
130
|
+
if e == 0:
|
|
131
|
+
return (-1)**s * (f / 2**23) * 2**(-126)
|
|
132
|
+
elif e == 255:
|
|
133
|
+
if f == 0:
|
|
134
|
+
return float('-inf') if s else float('inf')
|
|
135
|
+
return float('nan')
|
|
136
|
+
else:
|
|
137
|
+
return (-1)**s * (1 + f / 2**23) * 2**(e - 127)
|
|
138
|
+
|
|
139
|
+
elif bits == 64:
|
|
140
|
+
# IEEE754 double precision
|
|
141
|
+
s = (raw >> 63) & 0x1
|
|
142
|
+
e = (raw >> 52) & 0x7FF
|
|
143
|
+
f = raw & 0xFFFFFFFFFFFFF
|
|
144
|
+
|
|
145
|
+
if e == 0:
|
|
146
|
+
return (-1)**s * (f / 2**52) * 2**(-1022)
|
|
147
|
+
elif e == 2047:
|
|
148
|
+
if f == 0:
|
|
149
|
+
return float('-inf') if s else float('inf')
|
|
150
|
+
return float('nan')
|
|
151
|
+
else:
|
|
152
|
+
return (-1)**s * (1 + f / 2**52) * 2**(e - 1023)
|
|
153
|
+
|
|
154
|
+
else:
|
|
155
|
+
raise ValueError(''.join([
|
|
156
|
+
f"Unsupported bit size for float conversion: {bits}. ",
|
|
157
|
+
"Only 16, 32, and 64 are supported."
|
|
158
|
+
]))
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Tests for sltcore.bits."""
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from sltcore.bits import _required_bytes_for_extraction, bits_get, bits_set
|
|
5
|
+
from sltcore.types import InfoSize
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_required_bytes_for_extraction():
|
|
9
|
+
"""Test the _required_bytes_for_extraction function
|
|
10
|
+
with various scenarios."""
|
|
11
|
+
# offset.bit + size.bits < 8 → 1 byte
|
|
12
|
+
assert _required_bytes_for_extraction(InfoSize(0, 0), InfoSize(0, 7)) == 1
|
|
13
|
+
|
|
14
|
+
# Exactly 8 → 1 byte
|
|
15
|
+
assert _required_bytes_for_extraction(InfoSize(0, 0), InfoSize(0, 8)) == 1
|
|
16
|
+
|
|
17
|
+
# 9 → 2 bytes
|
|
18
|
+
assert _required_bytes_for_extraction(InfoSize(0, 0), InfoSize(0, 9)) == 2
|
|
19
|
+
|
|
20
|
+
# With offset.bit
|
|
21
|
+
assert _required_bytes_for_extraction(InfoSize(0, 3), InfoSize(0, 5)) == 1
|
|
22
|
+
assert _required_bytes_for_extraction(InfoSize(0, 6), InfoSize(0, 4)) == 2
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_bits_get_single_byte():
|
|
26
|
+
"""Test bits_get with a single byte buffer."""
|
|
27
|
+
buf = bytearray([0b11010110]) # 214
|
|
28
|
+
offset = InfoSize(0, 2) # bit offset = 2
|
|
29
|
+
size = InfoSize(0, 4) # 4 bits
|
|
30
|
+
|
|
31
|
+
info = bits_get(buf, offset, size)
|
|
32
|
+
assert info.raw_value == 0b0101 # 214 = 11010110 → offset=2 → 0101
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_bits_get_cross_byte():
|
|
36
|
+
"""Test bits_get with a buffer that spans multiple bytes."""
|
|
37
|
+
buf = bytearray([0b11110000, 0b00001111])
|
|
38
|
+
offset = InfoSize(0, 6) # From bit 6 of byte0
|
|
39
|
+
size = InfoSize(0, 6) # Extract 6 bits (spans into byte1)
|
|
40
|
+
|
|
41
|
+
info = bits_get(buf, offset, size)
|
|
42
|
+
# 11110000 00001111
|
|
43
|
+
# ^^ ^^^^
|
|
44
|
+
assert info.raw_value == 0b000000 # front-packed
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_bits_set_single_byte():
|
|
48
|
+
"""Test bits_set with a single byte buffer."""
|
|
49
|
+
buf = bytearray([0b11110000])
|
|
50
|
+
offset = InfoSize(0, 2)
|
|
51
|
+
size = InfoSize(0, 4)
|
|
52
|
+
|
|
53
|
+
bits_set(buf, offset, size, 0b0011)
|
|
54
|
+
# 11110000 → offset=2 → bits to change: 1100 → write 0011
|
|
55
|
+
assert buf[0] == 0b11001100
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_bits_set_cross_byte():
|
|
59
|
+
"""Test bits_set with a buffer that spans multiple bytes."""
|
|
60
|
+
buf = bytearray([0b11110000, 0b00001111])
|
|
61
|
+
offset = InfoSize(0, 6)
|
|
62
|
+
size = InfoSize(0, 6)
|
|
63
|
+
|
|
64
|
+
bits_set(buf, offset, size, 0b101010)
|
|
65
|
+
|
|
66
|
+
# Comparing with a manually calculated mask is fine, but
|
|
67
|
+
# confirming it matches by re-extracting with bits_get
|
|
68
|
+
# is more abstract and robust
|
|
69
|
+
info = bits_get(buf, offset, size)
|
|
70
|
+
|
|
71
|
+
assert info.raw_value == 0b101010
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_bits_get_set_roundtrip():
|
|
75
|
+
"""Test that bits_get followed by bits_set returns the original value."""
|
|
76
|
+
buf = bytearray([0xAA, 0xBB, 0xCC, 0xDD])
|
|
77
|
+
|
|
78
|
+
offset = InfoSize(0, 5)
|
|
79
|
+
size = InfoSize(0, 10)
|
|
80
|
+
|
|
81
|
+
# Verify original value is restored through get -> set cycle
|
|
82
|
+
original = bits_get(buf, offset, size).raw_value
|
|
83
|
+
|
|
84
|
+
bits_set(buf, offset, size, original)
|
|
85
|
+
after = bits_get(buf, offset, size).raw_value
|
|
86
|
+
|
|
87
|
+
assert original == after
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Tests for sltcore.types."""
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from sltcore.types import Info, InfoSize
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_infosize_normalization():
|
|
8
|
+
"""Test InfoSize normalization of bits to bytes."""
|
|
9
|
+
# 10 bits should become 1 byte and 2 bits
|
|
10
|
+
size = InfoSize(0, 10)
|
|
11
|
+
assert size.byte == 1
|
|
12
|
+
assert size.bit == 2
|
|
13
|
+
|
|
14
|
+
# 16 bits should become 2 bytes and 0 bits
|
|
15
|
+
size = InfoSize(1, 8)
|
|
16
|
+
assert size.byte == 2
|
|
17
|
+
assert size.bit == 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_infosize_bits_property():
|
|
21
|
+
"""Test the bits property of InfoSize."""
|
|
22
|
+
size = InfoSize(2, 3)
|
|
23
|
+
assert size.bits == 19 # 2 * 8 + 3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_infosize_bytes_property():
|
|
27
|
+
"""Test the bytes property of InfoSize (rounding up)."""
|
|
28
|
+
assert InfoSize(1, 0).bytes == 1
|
|
29
|
+
assert InfoSize(1, 1).bytes == 2
|
|
30
|
+
assert InfoSize(1, 7).bytes == 2
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_infosize_hex_digits():
|
|
34
|
+
"""Test the hex_digits property."""
|
|
35
|
+
assert InfoSize(1, 0).hex_digits == 2 # 8 bits / 4
|
|
36
|
+
assert InfoSize(2, 0).hex_digits == 4 # 16 bits / 4
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_infosize_mask():
|
|
40
|
+
"""Test the mask property."""
|
|
41
|
+
assert InfoSize(0, 3).mask == 0b111
|
|
42
|
+
assert InfoSize(1, 0).mask == 0xFF
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_infosize_arithmetic():
|
|
46
|
+
"""Test addition and subtraction of InfoSize."""
|
|
47
|
+
s1 = InfoSize(1, 2)
|
|
48
|
+
s2 = InfoSize(0, 7)
|
|
49
|
+
|
|
50
|
+
# Addition
|
|
51
|
+
added = s1 + s2
|
|
52
|
+
assert added.bits == s1.bits + s2.bits
|
|
53
|
+
|
|
54
|
+
# Subtraction
|
|
55
|
+
subbed = s1 - s2
|
|
56
|
+
assert subbed.bits == s1.bits - s2.bits
|
|
57
|
+
|
|
58
|
+
with pytest.raises(ValueError):
|
|
59
|
+
s2 - s1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_info_to_signed_int():
|
|
63
|
+
"""Test signed integer conversion."""
|
|
64
|
+
# 8-bit signed
|
|
65
|
+
info = Info(0xFF, InfoSize(1, 0))
|
|
66
|
+
assert info.to_signed_int == -1
|
|
67
|
+
|
|
68
|
+
info = Info(0x7F, InfoSize(1, 0))
|
|
69
|
+
assert info.to_signed_int == 127
|
|
70
|
+
|
|
71
|
+
# 4-bit signed
|
|
72
|
+
info = Info(0b1111, InfoSize(0, 4))
|
|
73
|
+
assert info.to_signed_int == -1
|
|
74
|
+
|
|
75
|
+
info = Info(0b0111, InfoSize(0, 4))
|
|
76
|
+
assert info.to_signed_int == 7
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_info_to_bytes():
|
|
80
|
+
"""Test conversion to bytes object."""
|
|
81
|
+
info = Info(0xAABB, InfoSize(2, 0))
|
|
82
|
+
assert info.to_bytes == b'\xaa\xbb'
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_info_to_float_16():
|
|
86
|
+
"""Test 16-bit float conversion."""
|
|
87
|
+
# 0.0
|
|
88
|
+
assert Info(0x0000, InfoSize(0, 16)).to_float == 0.0
|
|
89
|
+
# 1.0 (sign=0, exp=15, fraction=0) -> 0 01111 0000000000 -> 0x3C00
|
|
90
|
+
assert Info(0x3C00, InfoSize(0, 16)).to_float == 1.0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_info_to_float_32():
|
|
94
|
+
"""Test 32-bit float conversion."""
|
|
95
|
+
# 1.0 (sign=0, exp=127, fraction=0) -> 0x3F800000
|
|
96
|
+
assert Info(0x3F800000, InfoSize(0, 32)).to_float == 1.0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_info_to_float_64():
|
|
100
|
+
"""Test 64-bit float conversion."""
|
|
101
|
+
# 1.0 (sign=0, exp=1023, fraction=0) -> 0x3FF0000000000000
|
|
102
|
+
assert Info(0x3FF0000000000000, InfoSize(0, 64)).to_float == 1.0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_info_to_float_invalid_size():
|
|
106
|
+
"""Test that invalid bit sizes for float conversion raise ValueError."""
|
|
107
|
+
with pytest.raises(ValueError):
|
|
108
|
+
Info(0, InfoSize(0, 10)).to_float
|
sltcore-0.1.0/uv.lock
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.12"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "colorama"
|
|
7
|
+
version = "0.4.6"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
10
|
+
wheels = [
|
|
11
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "iniconfig"
|
|
16
|
+
version = "2.3.0"
|
|
17
|
+
source = { registry = "https://pypi.org/simple" }
|
|
18
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
19
|
+
wheels = [
|
|
20
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "packaging"
|
|
25
|
+
version = "26.2"
|
|
26
|
+
source = { registry = "https://pypi.org/simple" }
|
|
27
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
|
28
|
+
wheels = [
|
|
29
|
+
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[[package]]
|
|
33
|
+
name = "pluggy"
|
|
34
|
+
version = "1.6.0"
|
|
35
|
+
source = { registry = "https://pypi.org/simple" }
|
|
36
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
37
|
+
wheels = [
|
|
38
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[[package]]
|
|
42
|
+
name = "pygments"
|
|
43
|
+
version = "2.20.0"
|
|
44
|
+
source = { registry = "https://pypi.org/simple" }
|
|
45
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
|
46
|
+
wheels = [
|
|
47
|
+
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[[package]]
|
|
51
|
+
name = "pytest"
|
|
52
|
+
version = "9.1.1"
|
|
53
|
+
source = { registry = "https://pypi.org/simple" }
|
|
54
|
+
dependencies = [
|
|
55
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
56
|
+
{ name = "iniconfig" },
|
|
57
|
+
{ name = "packaging" },
|
|
58
|
+
{ name = "pluggy" },
|
|
59
|
+
{ name = "pygments" },
|
|
60
|
+
]
|
|
61
|
+
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
|
62
|
+
wheels = [
|
|
63
|
+
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[[package]]
|
|
67
|
+
name = "sltcore"
|
|
68
|
+
version = "0.1.0"
|
|
69
|
+
source = { editable = "." }
|
|
70
|
+
|
|
71
|
+
[package.dev-dependencies]
|
|
72
|
+
dev = [
|
|
73
|
+
{ name = "pytest" },
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[package.metadata]
|
|
77
|
+
|
|
78
|
+
[package.metadata.requires-dev]
|
|
79
|
+
dev = [{ name = "pytest", specifier = ">=9.1.1" }]
|