compass-lib 0.0.1__py3-none-any.whl → 0.0.3__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.
Files changed (38) hide show
  1. compass_lib/__init__.py +115 -3
  2. compass_lib/commands/__init__.py +2 -1
  3. compass_lib/commands/convert.py +225 -32
  4. compass_lib/commands/encrypt.py +115 -0
  5. compass_lib/commands/geojson.py +118 -0
  6. compass_lib/commands/main.py +4 -2
  7. compass_lib/constants.py +84 -0
  8. compass_lib/enums.py +309 -65
  9. compass_lib/errors.py +86 -0
  10. compass_lib/geo_utils.py +47 -0
  11. compass_lib/geojson.py +1024 -0
  12. compass_lib/interface.py +332 -0
  13. compass_lib/io.py +246 -0
  14. compass_lib/models.py +251 -0
  15. compass_lib/plot/__init__.py +28 -0
  16. compass_lib/plot/models.py +265 -0
  17. compass_lib/plot/parser.py +610 -0
  18. compass_lib/project/__init__.py +36 -0
  19. compass_lib/project/format.py +158 -0
  20. compass_lib/project/models.py +494 -0
  21. compass_lib/project/parser.py +638 -0
  22. compass_lib/survey/__init__.py +24 -0
  23. compass_lib/survey/format.py +284 -0
  24. compass_lib/survey/models.py +160 -0
  25. compass_lib/survey/parser.py +842 -0
  26. compass_lib/validation.py +74 -0
  27. compass_lib-0.0.3.dist-info/METADATA +60 -0
  28. compass_lib-0.0.3.dist-info/RECORD +31 -0
  29. {compass_lib-0.0.1.dist-info → compass_lib-0.0.3.dist-info}/WHEEL +1 -3
  30. compass_lib-0.0.3.dist-info/entry_points.txt +8 -0
  31. compass_lib/parser.py +0 -282
  32. compass_lib/section.py +0 -18
  33. compass_lib/shot.py +0 -21
  34. compass_lib-0.0.1.dist-info/METADATA +0 -268
  35. compass_lib-0.0.1.dist-info/RECORD +0 -14
  36. compass_lib-0.0.1.dist-info/entry_points.txt +0 -5
  37. compass_lib-0.0.1.dist-info/top_level.txt +0 -1
  38. {compass_lib-0.0.1.dist-info → compass_lib-0.0.3.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,74 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Validation utilities for Compass data.
3
+
4
+ This module provides validation functions for station names and other
5
+ data elements based on Compass format specifications.
6
+ """
7
+
8
+ import calendar
9
+ import re
10
+ from re import Pattern
11
+
12
+ # Station name pattern: printable ASCII characters (0x21-0x7F), no spaces
13
+ # The original Compass spec limited to 12 chars, but modern files often exceed this
14
+ # We allow any length but still require valid printable ASCII
15
+ STATION_NAME_PATTERN: Pattern[str] = re.compile(r"^[\x21-\x7f]+$")
16
+
17
+
18
+ def is_valid_station_name(name: str) -> bool:
19
+ """Check if a station name is valid.
20
+
21
+ Valid station names:
22
+ - 1 or more characters (no upper limit enforced)
23
+ - Printable ASCII only (0x21-0x7F)
24
+ - No spaces or control characters
25
+
26
+ Note: The original Compass format limited station names to 12 characters,
27
+ but many modern Compass files use longer names. This validation accepts
28
+ any length to ensure compatibility with real-world data.
29
+
30
+ Args:
31
+ name: Station name to validate
32
+
33
+ Returns:
34
+ True if valid, False otherwise
35
+ """
36
+ if not name:
37
+ return False
38
+ return bool(STATION_NAME_PATTERN.match(name))
39
+
40
+
41
+ def validate_station_name(name: str) -> None:
42
+ """Validate a station name, raising an error if invalid.
43
+
44
+ Args:
45
+ name: Station name to validate
46
+
47
+ Raises:
48
+ ValueError: If the station name is invalid
49
+ """
50
+ if not is_valid_station_name(name):
51
+ # Escape non-printable characters for error message
52
+ escaped = ""
53
+ for char in name:
54
+ if ord(char) < 0x20 or ord(char) > 0x7F:
55
+ escaped += f"\\x{ord(char):02x}"
56
+ else:
57
+ escaped += char
58
+ msg = f"Invalid station name: {escaped}"
59
+ raise ValueError(msg)
60
+
61
+
62
+ def days_in_month(month: int, year: int) -> int:
63
+ """Get the number of days in a month, accounting for leap years.
64
+
65
+ Args:
66
+ month: Month (1-12)
67
+ year: Year
68
+
69
+ Returns:
70
+ Number of days in the month
71
+ """
72
+ # monthrange returns (weekday_of_first_day, num_days_in_month)
73
+ _, num_days = calendar.monthrange(year, month)
74
+ return num_days
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: compass_lib
3
+ Version: 0.0.3
4
+ Summary: Compass Parser Library.
5
+ Keywords: cave,survey,karst
6
+ Author-email: Jonathan Dekhtiar <jonathan@dekhtiar.com>
7
+ Maintainer-email: Jonathan Dekhtiar <jonathan@dekhtiar.com>
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: Information Technology
14
+ Classifier: Topic :: Software Development :: Build Tools
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Topic :: Utilities
18
+ Classifier: License :: OSI Approved :: Apache Software License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ License-File: LICENSE
23
+ Requires-Dist: geojson>=3.2,<4
24
+ Requires-Dist: orjson>=3.10,<3.12
25
+ Requires-Dist: pydantic>=2.12,<2.13
26
+ Requires-Dist: pydantic-extra-types>=2.11,<3.0
27
+ Requires-Dist: pyIGRF14==1.0.4
28
+ Requires-Dist: pyproj>=3.7.1,<3.8
29
+ Requires-Dist: utm>=0.8.1,<0.9
30
+ Requires-Dist: cryptography>=44.0.0,<47.0.0 ; extra == "test"
31
+ Requires-Dist: python-dotenv>=1.0.0,<2.0.0 ; extra == "test"
32
+ Requires-Dist: deepdiff>=7.0,<9.0 ; extra == "test"
33
+ Requires-Dist: pytest>=8.0.0,<10.0.0 ; extra == "test"
34
+ Requires-Dist: pytest-cov>=5.0.0,<8.0.0 ; extra == "test"
35
+ Requires-Dist: pytest-dotenv>=0.5.0,<1.0.0 ; extra == "test"
36
+ Requires-Dist: pytest-env>=1.1.3,<2.0.0 ; extra == "test"
37
+ Requires-Dist: pytest-runner>=6.0.0,<7.0.0 ; extra == "test"
38
+ Requires-Dist: pytest-ordering>=0.6,<1.0.0 ; extra == "test"
39
+ Requires-Dist: parameterized>=0.9.0,<0.10 ; extra == "test"
40
+ Project-URL: Bug Reports, https://github.com/OpenSpeleo/pytool_compass_lib/issues
41
+ Project-URL: Homepage, https://pypi.org/project/compass-lib/
42
+ Project-URL: Source, https://github.com/OpenSpeleo/pytool_compass_lib
43
+ Provides-Extra: test
44
+
45
+ # Compass Python Lib
46
+
47
+ ## Conversion commands:
48
+
49
+ ```bash
50
+ # Install in dev mod
51
+ pip install -e ".[dev,test]"
52
+
53
+ # Install latest stable version
54
+ pip install compass_lib
55
+
56
+ # run some commands
57
+ compass convert --input_file=./tests/artifacts/fulford.dat --output_file=fulford.json --format=json --overwrite
58
+ compass convert --input_file=./tests/artifacts/random.dat --output_file=random.json --format=json --overwrite
59
+ ```
60
+
@@ -0,0 +1,31 @@
1
+ compass_lib/__init__.py,sha256=GmS4B4rae0P8zy4-givpmeyFUSqKRJ8WEERgxro9Gxs,3594
2
+ compass_lib/constants.py,sha256=TApFKLFXywSS_iFx_UBBO9mQhjW2-1f8Wsu0J-Mr-rU,3144
3
+ compass_lib/enums.py,sha256=PgIOhqb6qUBHZhZpVaZfQ_qG6DCp2nfQD4SghM-lAoM,9667
4
+ compass_lib/errors.py,sha256=0D5jCIroPdRX2oVx1PK_lotYKDdjc_94jeFKQhW8pBE,2473
5
+ compass_lib/geo_utils.py,sha256=5KKHKNIGOXrV3dTGbaLbGY4j7EajFXf0bcKFsDyJ0Sg,1409
6
+ compass_lib/geojson.py,sha256=yROsA_J19Afkx-_i3E2TFL2Qi28uAhhAfjLWmpzDoFA,34289
7
+ compass_lib/interface.py,sha256=GcM2yRD39dpy94brTIpJD-Z5DSlmBjpuu6dkTsHpyoI,10576
8
+ compass_lib/io.py,sha256=xU8JYg0gQK6FrpvEQHqXHCBotIWNVJP2Iu1MidgBbQ8,6506
9
+ compass_lib/models.py,sha256=vXQ-kJZVFRmUsNV3-8bsdTC48hLmkuoOIsIco8_al9Y,7036
10
+ compass_lib/validation.py,sha256=tfLn3MNblpQ7vb0TDeimEQV_zeNkLMT8NY0xCdUQFOo,2176
11
+ compass_lib/commands/__init__.py,sha256=kHsx7hB3ivV6wfdRn1KkhlbVPkLcaGsMLXOdgZhKU3Q,79
12
+ compass_lib/commands/convert.py,sha256=RZ3ZFd83yAb2pgso6oaRRtHjEHMSojsaoKmo4RX6fcs,8501
13
+ compass_lib/commands/encrypt.py,sha256=dD5IspbWPnjFjiFDa5RBkfgrQzG_5VglPKGqIOk_Uf8,3189
14
+ compass_lib/commands/geojson.py,sha256=kRgukxnGJIFvFod9odJFaMBZE6BK0I3polNZyBx9G9s,3554
15
+ compass_lib/commands/main.py,sha256=rce8oDfTwZdR5oKVQggNeGn8geIldPRbjvqRucwnFAI,775
16
+ compass_lib/plot/__init__.py,sha256=YXIKLAJSmHtIxiZNxCkDJLR3ud8Dgeea7pKrchB_NtY,953
17
+ compass_lib/plot/models.py,sha256=OEbSibClG95Jo-rDET2chAcfuE0qLsfnj522WW3jmtY,7815
18
+ compass_lib/plot/parser.py,sha256=K3YK-CVXCxAUhLQpi95CajPvWCWOwv41d05Whylysxg,19294
19
+ compass_lib/project/__init__.py,sha256=qdYdBCdEP669a4zT3r7xHm6qDGquf52hkeMyKzXMx_c,1312
20
+ compass_lib/project/format.py,sha256=uxSNG97yzwfUZpV7uYS8n6q0cZEzfSuKi8igBhZwKGY,5033
21
+ compass_lib/project/models.py,sha256=Z3qDggeEV_SEoO5ky5Wjrc7ZmyNdxnTSolNSIpoyQb0,15033
22
+ compass_lib/project/parser.py,sha256=YV3_qUMNbd_enql_5BKcU8jithltTzSmdhbgpSBNvIU,21733
23
+ compass_lib/survey/__init__.py,sha256=9BfBbW0luUr1ldNI4joD1WuZpTH7wuQ2lsdyMw-ppYY,784
24
+ compass_lib/survey/format.py,sha256=L5DPPe3riTknro2a_V70n-jkdJOkp9wK6TX0KbMjrPM,8655
25
+ compass_lib/survey/models.py,sha256=YRch52D0kBf9SxQ5RIhbvIy09-Yt2AtNeNRugJd7K_c,5258
26
+ compass_lib/survey/parser.py,sha256=xvBi3jr1Hgq3rzRzWTYJFQaAHpv7jFtD-QBeyhmAjhM,27402
27
+ compass_lib-0.0.3.dist-info/entry_points.txt,sha256=VhEdfsIVxwPerPEyPYms1IIJ6r4UOeCgPagV2BMzUd8,216
28
+ compass_lib-0.0.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
29
+ compass_lib-0.0.3.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
30
+ compass_lib-0.0.3.dist-info/METADATA,sha256=9oGhJp4nt62bUfzsDLYJgujs7c-kVELSR7Nc1k1kEv8,2393
31
+ compass_lib-0.0.3.dist-info/RECORD,,
@@ -1,6 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.1.1)
2
+ Generator: flit 3.12.0
3
3
  Root-Is-Purelib: true
4
- Tag: py2-none-any
5
4
  Tag: py3-none-any
6
-
@@ -0,0 +1,8 @@
1
+ [compass_lib.actions]
2
+ convert=compass_lib.commands.convert:convert
3
+ encrypt=compass_lib.commands.encrypt:encrypt
4
+ geojson=compass_lib.commands.geojson:geojson
5
+
6
+ [console_scripts]
7
+ compass=compass_lib.commands.main:main
8
+
compass_lib/parser.py DELETED
@@ -1,282 +0,0 @@
1
- #!/usr/bin/env python
2
-
3
- import re
4
- import datetime
5
- import hashlib
6
- import json
7
- from typing import Optional, Union
8
-
9
- from pathlib import Path
10
- from compass_lib.enums import ShotFlag
11
-
12
- from compass_lib.section import SurveySection
13
- from compass_lib.shot import SurveyShot
14
-
15
- from functools import cached_property
16
-
17
-
18
- import dataclasses
19
- from dataclasses import dataclass
20
-
21
- # ============================== CompassFileFormat ============================== #
22
-
23
- # _formatFormat(): string {
24
- # const {
25
- # displayAzimuthUnit,
26
- # displayLengthUnit,
27
- # displayLrudUnit,
28
- # displayInclinationUnit,
29
- # lrudOrder,
30
- # shotMeasurementOrder,
31
- # hasBacksights,
32
- # lrudAssociation,
33
- # } = this
34
- # return `${inverseAzimuthUnits[displayAzimuthUnit]}${
35
- # inverseLengthUnits[displayLengthUnit]
36
- # }${inverseLengthUnits[displayLrudUnit]}${
37
- # inverseInclinationUnits[displayInclinationUnit]
38
- # }${lrudOrder
39
- # .map(i => inverseLrudItems[i])
40
- # .join('')}${shotMeasurementOrder
41
- # .map(i => inverseShotMeasurementItems[i])
42
- # .join('')}${hasBacksights ? 'B' : 'N'}${
43
- # lrudAssociation != null ? inverseStationSides[lrudAssociation] : ''
44
- # }`
45
- # }
46
-
47
- @dataclass
48
- class CompassFileFormat:
49
- displayAzimuthUnit: str
50
- displayLengthUnit: str
51
- displayLrudUnit: str
52
- displayInclinationUnit: str
53
- lrudOrder: str
54
- shotMeasurementOrder: str
55
- hasBacksights: str
56
- lrudAssociation: str
57
-
58
- @classmethod
59
- def from_str(cls, input):
60
- return cls(
61
- displayAzimuthUnit="",
62
- displayLengthUnit="",
63
- displayLrudUnit="",
64
- displayInclinationUnit="",
65
- lrudOrder="",
66
- shotMeasurementOrder="",
67
- hasBacksights="",
68
- lrudAssociation="",
69
- )
70
-
71
-
72
-
73
- class EnhancedJSONEncoder(json.JSONEncoder):
74
- def default(self, obj):
75
-
76
- if dataclasses.is_dataclass(obj):
77
- return dataclasses.asdict(obj)
78
-
79
- if isinstance(obj, datetime.date):
80
- return obj.isoformat()
81
-
82
- if isinstance(obj, ShotFlag):
83
- return obj.value
84
-
85
- return super().default(obj)
86
-
87
-
88
- class CompassParser:
89
- SEPARATOR = "\f" # Form_feed: https://www.ascii-code.com/12
90
- END_OF_FILE = "\x1A" # Substitute: https://www.ascii-code.com/26
91
-
92
- def __init__(self, filepath: str) -> None:
93
-
94
- self._filepath = Path(filepath)
95
-
96
- if not self.filepath.is_file():
97
- raise FileNotFoundError(f"File not found: {filepath}")
98
-
99
- # Ensure at least that the file type is valid
100
- _ = self._data
101
-
102
- # =================== Data Loading =================== #
103
-
104
- @cached_property
105
- def _data(self):
106
-
107
- with self.filepath.open(mode="r") as f:
108
- data = f.read()
109
-
110
- return [
111
- activity.strip()
112
- for activity in data.split(CompassParser.SEPARATOR)
113
- if CompassParser.END_OF_FILE not in activity
114
- ]
115
-
116
- # =================== File Properties =================== #
117
-
118
- def __repr__(self) -> str:
119
- repr = f"[CompassSurveyFile {self.filetype.upper()}] `{self.filepath}`:"
120
- # for key in self._KEY_MAP.keys():
121
- # if key.startswith("_"):
122
- # continue
123
- # repr += f"\n\t- {key}: {getattr(self, key)}"
124
- # repr += f"\n\t- shots: Total Shots: {len(self.shots)}"
125
- # repr += f"\n\t- hash: {self.hash}"
126
- return repr
127
-
128
- @cached_property
129
- def __hash__(self):
130
- # return hashlib.sha256(self._as_binary()).hexdigest()
131
- return hashlib.sha256("0".encode()).hexdigest()
132
-
133
- @property
134
- def hash(self):
135
- return self.__hash__
136
-
137
- # =============== Descriptive Properties =============== #
138
-
139
- @property
140
- def filepath(self):
141
- return self._filepath
142
-
143
- @property
144
- def filetype(self):
145
- return self.filepath.suffix[1:]
146
- # try:
147
- # return ArianeFileType.from_str(self.filepath.suffix[1:])
148
- # except ValueError as e:
149
- # raise TypeError(e) from e
150
-
151
- @property
152
- def lstat(self):
153
- return self.filepath.lstat()
154
-
155
- @property
156
- def date_created(self):
157
- return self.lstat.st_ctime
158
-
159
- @property
160
- def date_last_modified(self):
161
- return self.lstat.st_mtime
162
-
163
- @property
164
- def date_last_opened(self):
165
- return self.lstat.st_atime
166
-
167
- # =================== Data Processing =================== #
168
-
169
- @cached_property
170
- def data(self):
171
- sections = []
172
- for activity in self._data:
173
- entries = activity.splitlines()
174
-
175
- cave_name = entries[0].strip()
176
-
177
- if "SURVEY NAME: " not in entries[1]:
178
- raise RuntimeError
179
- survey_name = entries[1].split(":")[-1].strip()
180
-
181
- date_str, comment_str = entries[2].split(" ", maxsplit=1)
182
-
183
- if "SURVEY DATE: " not in date_str:
184
- raise RuntimeError
185
- date = date_str.split(":")[-1].strip()
186
-
187
- if "COMMENT:" not in comment_str:
188
- raise RuntimeError
189
- survey_comment = comment_str.split(":")[-1].strip()
190
-
191
- if "SURVEY TEAM:" != entries[3].strip():
192
- raise RuntimeError
193
-
194
- surveyors = [suveyor.strip() for suveyor in entries[4].split(",") if suveyor.strip() != ""]
195
-
196
- if "DECLINATION:" not in entries[5]:
197
- raise RuntimeError
198
- if "FORMAT:" not in entries[5]:
199
- raise RuntimeError
200
- if "CORRECTIONS:" not in entries[5]:
201
- raise RuntimeError
202
-
203
- _, declination_str, _, format_str, _, correct_A, correct_B, correct_C = entries[5].split()
204
-
205
- shots = list()
206
- for shot in entries[9:]:
207
- shot_data = shot.split(maxsplit=9)
208
- from_id, to_id, length, bearing, incl, left, up, down, right = shot_data[:9]
209
-
210
- try:
211
- flags_comment = shot_data[9]
212
-
213
- flag_regex = rf"({ShotFlag.__start_token__}([{''.join(ShotFlag._value2member_map_.keys())}]*){ShotFlag.__end_token__})*(.*)"
214
- _, flag_str, comment = re.search(flag_regex, flags_comment).groups()
215
-
216
- flags = [ShotFlag._value2member_map_[f] for f in flag_str] if flag_str else None
217
-
218
- except IndexError:
219
- flags = None
220
- comment = None
221
-
222
- shots.append(SurveyShot(
223
- from_id=from_id,
224
- to_id=to_id,
225
- length=float(length),
226
- bearing=float(bearing),
227
- inclination=float(incl),
228
- left=float(left),
229
- up=float(up),
230
- down=float(down),
231
- right=float(right),
232
- flags=sorted(set(flags), key=lambda f: f.value) if flags else None,
233
- comment=comment.strip() if comment else None
234
- ))
235
-
236
- section = SurveySection(
237
- cave_name=cave_name,
238
- survey_name=survey_name,
239
- date=datetime.datetime.strptime(date, "%m %d %Y").date(),
240
- comment=survey_comment,
241
- surveyors=surveyors,
242
- declination=float(declination_str),
243
- format=format_str,
244
- correction=(float(correct_A), float(correct_B), float(correct_C)),
245
- shots=shots
246
- )
247
- sections.append(section)
248
-
249
- return sections
250
-
251
-
252
- # =================== Export Formats =================== #
253
-
254
- def to_json(self, filepath: Optional[Union[str, Path]] = None) -> str:
255
- json_str = json.dumps(self.data, indent=4, sort_keys=True, cls=EnhancedJSONEncoder)
256
-
257
- if filepath is not None:
258
- with open(filepath, mode="w") as file:
259
- file.write(json_str)
260
-
261
- return json_str
262
-
263
- # ==================== Public APIs ====================== #
264
-
265
- @cached_property
266
- def shots(self):
267
- return []
268
- # return [
269
- # SurveyShot(data=survey_shot)
270
- # for survey_shot in self._KEY_MAP.fetch(self._shots_list, "_shots")
271
- # ]
272
-
273
- @cached_property
274
- def sections(self):
275
- return []
276
- # section_map = dict()
277
- # for shot in self.shots:
278
- # try:
279
- # section_map[shot.section].add_shot(shot)
280
- # except KeyError:
281
- # section_map[shot.section] = SurveySection(shot=shot)
282
- # return list(section_map.values())
compass_lib/section.py DELETED
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env python
2
-
3
- from datetime import date
4
- from dataclasses import dataclass
5
- from compass_lib.shot import SurveyShot
6
-
7
-
8
- @dataclass
9
- class SurveySection:
10
- cave_name: str
11
- survey_name: str
12
- date: date
13
- comment: str
14
- surveyors: list[str]
15
- declination: float
16
- format: str
17
- correction: tuple[float, float, float]
18
- shots: list[SurveyShot]
compass_lib/shot.py DELETED
@@ -1,21 +0,0 @@
1
- #!/usr/bin/env python
2
-
3
- from dataclasses import dataclass
4
-
5
- from compass_lib.enums import ShotFlag
6
-
7
- @dataclass
8
- class SurveyShot:
9
- # FROM TO LENGTH BEARING INC LEFT UP DOWN RIGHT FLAGS COMMENTS
10
- # A1 A2 21.75 63.50 -28.00 2.60 2.60 2.60 2.60#
11
- from_id: str
12
- to_id: str
13
- length: float
14
- bearing: float
15
- inclination: float
16
- left: float
17
- up: float
18
- down: float
19
- right: float
20
- flags: list[ShotFlag]
21
- comment: str