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.
@@ -1,4 +1,5 @@
1
1
  .venv/*
2
2
  phext/__pycache__/*
3
3
  tests/__pycache__/*
4
- unit-test.phext
4
+ unit-test.phext
5
+ release-dist/
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
- @total_ordering
5
- @dataclass
6
- class Coordinate:
7
- library: int
8
- shelf: int
9
- series: int
10
- collection: int
11
- volume: int
12
- book: int
13
- chapter: int
14
- section: int
15
- scroll: int
16
-
17
- @classmethod
18
- def from_string(ignored, coord_str: str):
19
- try:
20
- parts = coord_str.strip().split("/")
21
- if len(parts) != 3:
22
- raise ValueError("Coordinate must conform to Phext Standard Addressing")
23
-
24
- nums = []
25
- for part in parts:
26
- nums.extend(int(x) for x in part.split("."))
27
-
28
- if len(nums) != 9:
29
- raise ValueError("Coordinate must have exactly 9 numeric parts")
30
-
31
- if any(n < 1 or n > 999 for n in nums):
32
- raise ValueError("All coordinate values must be between 1 and 999")
33
-
34
- return Coordinate(nums[0], nums[1], nums[2], nums[3], nums[4], nums[5], nums[6], nums[7], nums[8])
35
-
36
- except Exception as e:
37
- raise ValueError(f"Invalid coordinate string: {coord_str} {e}") from e
38
-
39
- def libraryBreak(self):
40
- self.library += 1
41
- self.shelf = 1
42
- self.series = 1
43
- self.collection = 1
44
- self.volume = 1
45
- self.book = 1
46
- self.chapter = 1
47
- self.section = 1
48
- self.scroll = 1
49
-
50
- def shelfBreak(self):
51
- self.shelf += 1
52
- self.series = 1
53
- self.collection = 1
54
- self.volume = 1
55
- self.book = 1
56
- self.chapter = 1
57
- self.section = 1
58
- self.scroll = 1
59
-
60
- def seriesBreak(self):
61
- self.series += 1
62
- self.collection = 1
63
- self.volume = 1
64
- self.book = 1
65
- self.chapter = 1
66
- self.section = 1
67
- self.scroll = 1
68
-
69
- def collectionBreak(self):
70
- self.collection += 1
71
- self.volume = 1
72
- self.book = 1
73
- self.chapter = 1
74
- self.section = 1
75
- self.scroll = 1
76
-
77
- def volumeBreak(self):
78
- self.volume += 1
79
- self.book = 1
80
- self.chapter = 1
81
- self.section = 1
82
- self.scroll = 1
83
-
84
- def bookBreak(self):
85
- self.book += 1
86
- self.chapter = 1
87
- self.section = 1
88
- self.scroll = 1
89
-
90
- def chapterBreak(self):
91
- self.chapter += 1
92
- self.section = 1
93
- self.scroll = 1
94
-
95
- def sectionBreak(self):
96
- self.section += 1
97
- self.scroll = 1
98
-
99
- def scrollBreak(self):
100
- self.scroll += 1
101
-
102
- def __init__(self, lb, sf, sr, cn, vm, bk, ch, sn, sc):
103
- self.library = int(lb)
104
- self.shelf = int(sf)
105
- self.series = int(sr)
106
- self.collection = int(cn)
107
- self.volume = int(vm)
108
- self.book = int(bk)
109
- self.chapter = int(ch)
110
- self.section = int(sn)
111
- self.scroll = int(sc)
112
-
113
- def copy_coordinate(self, other):
114
- self.library = int(other.library)
115
- self.shelf = int(other.shelf)
116
- self.series = int(other.series)
117
- self.collection = int(other.collection)
118
- self.volume = int(other.volume)
119
- self.book = int(other.book)
120
- self.chapter = int(other.chapter)
121
- self.section = int(other.section)
122
- self.scroll = int(other.scroll)
123
-
124
- def __str__(self) -> str:
125
- return f"{self.library}.{self.shelf}.{self.series}/" \
126
- f"{self.collection}.{self.volume}.{self.book}/" \
127
- f"{self.chapter}.{self.section}.{self.scroll}"
128
-
129
- def urlencoded(self) -> str:
130
- return f"{self.library}.{self.shelf}.{self.series};" \
131
- f"{self.collection}.{self.volume}.{self.book};" \
132
- f"{self.chapter}.{self.section}.{self.scroll}"
133
-
134
- def __eq__(self, other):
135
- 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
136
-
137
- def __lt__(self, other):
138
- if not isinstance(other, Coordinate):
139
- return NotImplemented
140
- return self.as_tuple() < other.as_tuple()
141
-
142
- def as_tuple(self) -> tuple:
143
- return (
144
- self.library, self.shelf, self.series,
145
- self.collection, self.volume, self.book,
146
- self.chapter, self.section, self.scroll
147
- )
148
-
149
- def __hash__(self):
150
- return hash((
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
  ))