phext 0.3.0__tar.gz → 0.3.2__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.
- {phext-0.3.0 → phext-0.3.2}/.gitignore +2 -1
- phext-0.3.2/PKG-INFO +141 -0
- phext-0.3.2/phext/__init__.py +1 -0
- {phext-0.3.0 → phext-0.3.2}/phext/coordinate.py +160 -153
- phext-0.3.2/phext/phext.py +480 -0
- {phext-0.3.0 → phext-0.3.2}/pyproject.toml +14 -4
- phext-0.3.2/readme.md +118 -0
- phext-0.3.2/test/bench.py +55 -0
- {phext-0.3.0 → phext-0.3.2}/test/verify.py +1 -1
- phext-0.3.0/phext/phext.py → phext-0.3.2/tests/legacy_phext.py +548 -545
- {phext-0.3.0 → phext-0.3.2}/tests/test_coordinate.py +207 -190
- {phext-0.3.0 → phext-0.3.2}/tests/test_phext.py +919 -919
- phext-0.3.2/tests/test_phokenize.py +229 -0
- phext-0.3.2/uv.lock +705 -0
- phext-0.3.0/PKG-INFO +0 -28
- phext-0.3.0/README.md +0 -5
- phext-0.3.0/phext/__init__.py +0 -1
- {phext-0.3.0 → phext-0.3.2}/phext/positionedScroll.py +0 -0
- {phext-0.3.0 → phext-0.3.2}/phext/range.py +0 -0
- {phext-0.3.0 → phext-0.3.2}/tests/test_positionedScroll.py +0 -0
- {phext-0.3.0 → phext-0.3.2}/tests/test_range.py +0 -0
phext-0.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: phext
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Pythonic implementation of phext
|
|
5
|
+
Project-URL: Homepage, https://phext.io
|
|
6
|
+
Project-URL: Repository, https://github.com/wbic16/libphext-py
|
|
7
|
+
Project-URL: Issues, https://github.com/wbic16/libphext-py/issues
|
|
8
|
+
Author-email: Will Bickford <wbic16@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: xxhash
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Phext
|
|
25
|
+
|
|
26
|
+
Welcome to the Pythonic port of phext.
|
|
27
|
+
|
|
28
|
+
This port is a downstream artifact that tracks the Rust-based implementation of Phext. See https://github.com/wbic16/libphext-rs for more details.
|
|
29
|
+
|
|
30
|
+
## Getting Started
|
|
31
|
+
|
|
32
|
+
To use phext in your Python project, simply add `phext` as a dependency.
|
|
33
|
+
|
|
34
|
+
```pip install phext```
|
|
35
|
+
|
|
36
|
+
Refer to the `tests` folder for usage information.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
import phext
|
|
40
|
+
from phext.coordinate import Coordinate
|
|
41
|
+
from phext.phext import Phext
|
|
42
|
+
phext = Phext()
|
|
43
|
+
test = "Hello\x17World"
|
|
44
|
+
coord = Coordinate.from_string("1.1.1/1.1.1/1.1.1")
|
|
45
|
+
scroll = phext.fetch(test, coord)
|
|
46
|
+
scrolls = phext.explode(test)
|
|
47
|
+
coord2 = Coordinate(1,1,1, 1,1,1, 1,1,2)
|
|
48
|
+
print(scroll + ", " + scrolls[coord2])
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
The project uses [uv](https://docs.astral.sh/uv/). From the repository root:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv sync
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This creates `.venv` with phext installed from this checkout (editable) and the `dev` dependency group (pytest).
|
|
60
|
+
|
|
61
|
+
### Test
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv run pytest
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
pytest comes from the `dev` group, so the tests run in the project's environment against this checkout. `pyproject.toml` also puts the checkout on pytest's path, so a pytest from somewhere else still tests this code rather than a `phext` it finds installed.
|
|
68
|
+
|
|
69
|
+
To time the core operations on a real file:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv run python test/bench.py path/to/file.phext
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
It reports explode, implode, textmap, index, and fetch (of the last scroll). Name operations after the file to run only those, for example `explode fetch`.
|
|
76
|
+
|
|
77
|
+
### Build
|
|
78
|
+
|
|
79
|
+
1. Set the new version in both `pyproject.toml` and `phext/__init__.py`.
|
|
80
|
+
2. Build into an empty folder. `dist/` holds older builds that are checked into git, and `uv publish` would try to upload those too. Delete `release-dist` first if it is left over from a previous release.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv build --out-dir release-dist
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
3. Check the package metadata, then import the wheel in a throwaway environment (use the version you just built):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uvx twine check release-dist/*
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
uv run --isolated --no-project --with release-dist/phext-0.3.2-py3-none-any.whl python -c "import phext; print(phext.__version__)"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Publish
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
uv publish release-dist/*
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
This needs a PyPI API token for the `phext` project: set `UV_PUBLISH_TOKEN`, or pass `--token`. To rehearse on TestPyPI first (it uses a separate account and token):
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
uv publish --publish-url https://test.pypi.org/legacy/ release-dist/*
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Data Types
|
|
109
|
+
|
|
110
|
+
* `Coordinate`: A 9-tuple of the form library.shelf.series/collection.volume.book/chapter.section.scroll. Represents the location of a given scroll of text within a larger phext file.
|
|
111
|
+
* `Phext`: The main entrypoint to interacting with phexts.
|
|
112
|
+
* `fetch(buffer, coordinate)`: Fetches a scroll of text from the given serialized phext buffer
|
|
113
|
+
* `phokenize(buffer)`: Extracts a list of positioned scrolls from the given phext
|
|
114
|
+
* `append_scroll(entry, location)`: Emits the delimiters and text required to insert the positioned scroll, entry, into a phext stream (starting from the given location).
|
|
115
|
+
* `dephokenize(stack)`: Serializes a stack of positioned scrolls
|
|
116
|
+
* `normalize(str)`: Eliminates empty scrolls from a phext buffer
|
|
117
|
+
* `update(buffer, coord, scroll, overwrite)`: Supports insert and update operations - places the scroll of text at the given coordinate, taking the current state of the buffer into account
|
|
118
|
+
* `insert(buffer, coord, scroll)`: Inserts text at the end of the scroll located at coord within the buffer
|
|
119
|
+
* `replace(buffer, coord, scroll)`: Replaces the text located at coord within the buffer
|
|
120
|
+
* `remove(buffer, coord)`: Removes the contents of the scroll located at coord within the buffer
|
|
121
|
+
* `range_replace(buffer, range, text)`: Removes scrolls located within the given range, without shifting content outside of the range - this differs from libphext-rs by design - a pair of missing methods needs to be added to resolve the conflict
|
|
122
|
+
* `next_scroll(buffer, coord)`: Selects the next scroll after the given coordinate
|
|
123
|
+
* `get_subspace_coordinates(buffer, target)`: Retrieves the subspace offsets cooresponding to the given target coordinate within the buffer
|
|
124
|
+
* `merge(left, right)`: Zipper merges two phexts
|
|
125
|
+
* `subtract(left, right)`: Removes the scrolls from left that are present in right (as measured by coordinate matching)
|
|
126
|
+
* `expand(buffer)`: Dimensional increment for each phext break found in the buffer
|
|
127
|
+
* `contract(buffer)`: Dimensional decrement for each phext break found in the buffer
|
|
128
|
+
* `create_summary(buffer)`: Creates a one-line summary of a given scroll
|
|
129
|
+
* `navmap(urlbase, buffer)`: Creates an HTML navigation map of the given buffer
|
|
130
|
+
* `textmap(buffer)`: Creates a text-based navigation map of the given buffer
|
|
131
|
+
* `checksum(buffer)`: Computes an xxh3_128 hash of a given scroll
|
|
132
|
+
* `manifest(buffer)`: Computes a hierarchical hash of the given phext
|
|
133
|
+
* `soundex_v1(buffer)`: Computes a hierarchical soundex of the given phext
|
|
134
|
+
* `soundex_internal(buffer)`: Computes a low-level soundex of a given scroll
|
|
135
|
+
* `explode(buffer)`: Explodes a phext into a dictionary of Coordinate -> str
|
|
136
|
+
* `implode(hash)`: Implodes a dictionary of Coordinate -> str back into a phext
|
|
137
|
+
* `index_phokens(buffer)`: Computes the offsets of each scroll in the given phext
|
|
138
|
+
* `index(buffer)`: Computes a hierarchical map of scroll offsets in phext format
|
|
139
|
+
* `offset(buffer, coord)`: Computes the offset of a specific scroll in the given phext
|
|
140
|
+
* `PositionedScroll`: A pair of `Coordinate` and a scroll of text.
|
|
141
|
+
* `Range`: A pair of coordinates denoting an inclusive range across subspace
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.2"
|
|
@@ -1,154 +1,161 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from functools import total_ordering
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if len(
|
|
29
|
-
raise ValueError("Coordinate must
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
self.
|
|
48
|
-
self.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
self.
|
|
52
|
-
self.
|
|
53
|
-
self.
|
|
54
|
-
self.
|
|
55
|
-
self.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
self.
|
|
62
|
-
self.
|
|
63
|
-
self.
|
|
64
|
-
self.
|
|
65
|
-
self.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
self.
|
|
71
|
-
self.
|
|
72
|
-
self.
|
|
73
|
-
self.
|
|
74
|
-
self.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
self.volume
|
|
79
|
-
self.book = 1
|
|
80
|
-
self.chapter = 1
|
|
81
|
-
self.section = 1
|
|
82
|
-
self.scroll = 1
|
|
83
|
-
|
|
84
|
-
def
|
|
85
|
-
self.
|
|
86
|
-
self.
|
|
87
|
-
self.
|
|
88
|
-
self.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
self.
|
|
93
|
-
self.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
self.scroll
|
|
101
|
-
|
|
102
|
-
def
|
|
103
|
-
self.
|
|
104
|
-
self.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
self.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
self.
|
|
111
|
-
self.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
self.
|
|
115
|
-
self.
|
|
116
|
-
self.
|
|
117
|
-
self.
|
|
118
|
-
self.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
self.
|
|
122
|
-
self.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
def
|
|
150
|
-
return
|
|
151
|
-
self.library, self.shelf, self.series,
|
|
152
|
-
self.collection, self.volume, self.book,
|
|
153
|
-
self.chapter, self.section, self.scroll
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from functools import total_ordering
|
|
3
|
+
|
|
4
|
+
COORDINATE_MAXIMUM = 1000
|
|
5
|
+
|
|
6
|
+
def advance_coordinate(index: int) -> int:
|
|
7
|
+
# saturate at 999, matching libphext-rs: can't advance beyond the maximum
|
|
8
|
+
next = index + 1
|
|
9
|
+
return next if next < COORDINATE_MAXIMUM else index
|
|
10
|
+
|
|
11
|
+
@total_ordering
|
|
12
|
+
@dataclass
|
|
13
|
+
class Coordinate:
|
|
14
|
+
library: int
|
|
15
|
+
shelf: int
|
|
16
|
+
series: int
|
|
17
|
+
collection: int
|
|
18
|
+
volume: int
|
|
19
|
+
book: int
|
|
20
|
+
chapter: int
|
|
21
|
+
section: int
|
|
22
|
+
scroll: int
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_string(ignored, coord_str: str):
|
|
26
|
+
try:
|
|
27
|
+
parts = coord_str.strip().split("/")
|
|
28
|
+
if len(parts) != 3:
|
|
29
|
+
raise ValueError("Coordinate must conform to Phext Standard Addressing")
|
|
30
|
+
|
|
31
|
+
nums = []
|
|
32
|
+
for part in parts:
|
|
33
|
+
nums.extend(int(x) for x in part.split("."))
|
|
34
|
+
|
|
35
|
+
if len(nums) != 9:
|
|
36
|
+
raise ValueError("Coordinate must have exactly 9 numeric parts")
|
|
37
|
+
|
|
38
|
+
if any(n < 1 or n > 999 for n in nums):
|
|
39
|
+
raise ValueError("All coordinate values must be between 1 and 999")
|
|
40
|
+
|
|
41
|
+
return Coordinate(nums[0], nums[1], nums[2], nums[3], nums[4], nums[5], nums[6], nums[7], nums[8])
|
|
42
|
+
|
|
43
|
+
except Exception as e:
|
|
44
|
+
raise ValueError(f"Invalid coordinate string: {coord_str} {e}") from e
|
|
45
|
+
|
|
46
|
+
def libraryBreak(self):
|
|
47
|
+
self.library = advance_coordinate(self.library)
|
|
48
|
+
self.shelf = 1
|
|
49
|
+
self.series = 1
|
|
50
|
+
self.collection = 1
|
|
51
|
+
self.volume = 1
|
|
52
|
+
self.book = 1
|
|
53
|
+
self.chapter = 1
|
|
54
|
+
self.section = 1
|
|
55
|
+
self.scroll = 1
|
|
56
|
+
|
|
57
|
+
def shelfBreak(self):
|
|
58
|
+
self.shelf = advance_coordinate(self.shelf)
|
|
59
|
+
self.series = 1
|
|
60
|
+
self.collection = 1
|
|
61
|
+
self.volume = 1
|
|
62
|
+
self.book = 1
|
|
63
|
+
self.chapter = 1
|
|
64
|
+
self.section = 1
|
|
65
|
+
self.scroll = 1
|
|
66
|
+
|
|
67
|
+
def seriesBreak(self):
|
|
68
|
+
self.series = advance_coordinate(self.series)
|
|
69
|
+
self.collection = 1
|
|
70
|
+
self.volume = 1
|
|
71
|
+
self.book = 1
|
|
72
|
+
self.chapter = 1
|
|
73
|
+
self.section = 1
|
|
74
|
+
self.scroll = 1
|
|
75
|
+
|
|
76
|
+
def collectionBreak(self):
|
|
77
|
+
self.collection = advance_coordinate(self.collection)
|
|
78
|
+
self.volume = 1
|
|
79
|
+
self.book = 1
|
|
80
|
+
self.chapter = 1
|
|
81
|
+
self.section = 1
|
|
82
|
+
self.scroll = 1
|
|
83
|
+
|
|
84
|
+
def volumeBreak(self):
|
|
85
|
+
self.volume = advance_coordinate(self.volume)
|
|
86
|
+
self.book = 1
|
|
87
|
+
self.chapter = 1
|
|
88
|
+
self.section = 1
|
|
89
|
+
self.scroll = 1
|
|
90
|
+
|
|
91
|
+
def bookBreak(self):
|
|
92
|
+
self.book = advance_coordinate(self.book)
|
|
93
|
+
self.chapter = 1
|
|
94
|
+
self.section = 1
|
|
95
|
+
self.scroll = 1
|
|
96
|
+
|
|
97
|
+
def chapterBreak(self):
|
|
98
|
+
self.chapter = advance_coordinate(self.chapter)
|
|
99
|
+
self.section = 1
|
|
100
|
+
self.scroll = 1
|
|
101
|
+
|
|
102
|
+
def sectionBreak(self):
|
|
103
|
+
self.section = advance_coordinate(self.section)
|
|
104
|
+
self.scroll = 1
|
|
105
|
+
|
|
106
|
+
def scrollBreak(self):
|
|
107
|
+
self.scroll = advance_coordinate(self.scroll)
|
|
108
|
+
|
|
109
|
+
def __init__(self, lb, sf, sr, cn, vm, bk, ch, sn, sc):
|
|
110
|
+
self.library = int(lb)
|
|
111
|
+
self.shelf = int(sf)
|
|
112
|
+
self.series = int(sr)
|
|
113
|
+
self.collection = int(cn)
|
|
114
|
+
self.volume = int(vm)
|
|
115
|
+
self.book = int(bk)
|
|
116
|
+
self.chapter = int(ch)
|
|
117
|
+
self.section = int(sn)
|
|
118
|
+
self.scroll = int(sc)
|
|
119
|
+
|
|
120
|
+
def copy_coordinate(self, other):
|
|
121
|
+
self.library = int(other.library)
|
|
122
|
+
self.shelf = int(other.shelf)
|
|
123
|
+
self.series = int(other.series)
|
|
124
|
+
self.collection = int(other.collection)
|
|
125
|
+
self.volume = int(other.volume)
|
|
126
|
+
self.book = int(other.book)
|
|
127
|
+
self.chapter = int(other.chapter)
|
|
128
|
+
self.section = int(other.section)
|
|
129
|
+
self.scroll = int(other.scroll)
|
|
130
|
+
|
|
131
|
+
def __str__(self) -> str:
|
|
132
|
+
return f"{self.library}.{self.shelf}.{self.series}/" \
|
|
133
|
+
f"{self.collection}.{self.volume}.{self.book}/" \
|
|
134
|
+
f"{self.chapter}.{self.section}.{self.scroll}"
|
|
135
|
+
|
|
136
|
+
def urlencoded(self) -> str:
|
|
137
|
+
return f"{self.library}.{self.shelf}.{self.series};" \
|
|
138
|
+
f"{self.collection}.{self.volume}.{self.book};" \
|
|
139
|
+
f"{self.chapter}.{self.section}.{self.scroll}"
|
|
140
|
+
|
|
141
|
+
def __eq__(self, other):
|
|
142
|
+
return isinstance(self, Coordinate) and isinstance(other, Coordinate) and self.library == other.library and self.shelf == other.shelf and self.series == other.series and self.collection == other.collection and self.volume == other.volume and self.book == other.book and self.chapter == other.chapter and self.section == other.section and self.scroll == other.scroll
|
|
143
|
+
|
|
144
|
+
def __lt__(self, other):
|
|
145
|
+
if not isinstance(other, Coordinate):
|
|
146
|
+
return NotImplemented
|
|
147
|
+
return self.as_tuple() < other.as_tuple()
|
|
148
|
+
|
|
149
|
+
def as_tuple(self) -> tuple:
|
|
150
|
+
return (
|
|
151
|
+
self.library, self.shelf, self.series,
|
|
152
|
+
self.collection, self.volume, self.book,
|
|
153
|
+
self.chapter, self.section, self.scroll
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def __hash__(self):
|
|
157
|
+
return hash((
|
|
158
|
+
self.library, self.shelf, self.series,
|
|
159
|
+
self.collection, self.volume, self.book,
|
|
160
|
+
self.chapter, self.section, self.scroll
|
|
154
161
|
))
|