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,332 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Unified interface for Compass file I/O.
3
+
4
+ This module provides the primary entry point for reading and writing
5
+ Compass files. It follows the openspeleo_lib pattern:
6
+
7
+ 1. Parsers produce dictionaries (like loading JSON from disk)
8
+ 2. Dictionaries feed directly to Pydantic models via `model_validate()`
9
+ 3. Models serialize to dictionaries via `model_dump()`
10
+ 4. Formatters convert dictionaries to file content
11
+
12
+ This keeps parsing/formatting logic completely separate from Pydantic models.
13
+ """
14
+
15
+ from pathlib import Path
16
+ from typing import Any
17
+ from typing import Protocol
18
+
19
+ from compass_lib.constants import COMPASS_ENCODING
20
+ from compass_lib.constants import JSON_ENCODING
21
+ from compass_lib.project.format import format_mak_file
22
+ from compass_lib.project.models import CompassMakFile
23
+ from compass_lib.project.parser import CompassProjectParser
24
+ from compass_lib.survey.format import format_dat_file
25
+ from compass_lib.survey.models import CompassDatFile
26
+ from compass_lib.survey.parser import CompassSurveyParser
27
+
28
+ # Re-export for backwards compatibility
29
+ DEFAULT_ENCODING = COMPASS_ENCODING
30
+
31
+
32
+ class ProgressCallback(Protocol):
33
+ """Protocol for progress callbacks."""
34
+
35
+ def __call__(
36
+ self,
37
+ message: str | None = None,
38
+ completed: int | None = None,
39
+ total: int | None = None,
40
+ ) -> None:
41
+ """Report progress."""
42
+ ...
43
+
44
+
45
+ class CancellationToken:
46
+ """Token for checking if an operation should be cancelled."""
47
+
48
+ def __init__(self) -> None:
49
+ self._cancelled = False
50
+
51
+ @property
52
+ def cancelled(self) -> bool:
53
+ """Check if cancellation was requested."""
54
+ return self._cancelled
55
+
56
+ def cancel(self) -> None:
57
+ """Request cancellation."""
58
+ self._cancelled = True
59
+
60
+
61
+ class CompassInterface:
62
+ """Unified interface for Compass file I/O.
63
+
64
+ This class provides all file I/O operations for Compass files,
65
+ following the pattern:
66
+ - Reading: File → Parser → Dictionary → model_validate() → Model
67
+ - Writing: Model → model_dump() → Dictionary → Formatter → File
68
+
69
+ Example:
70
+ # Load a complete project
71
+ project = CompassInterface.load_project(Path("cave.mak"))
72
+
73
+ # Access nested data
74
+ for file_dir in project.file_directives:
75
+ if file_dir.data:
76
+ for trip in file_dir.data.trips:
77
+ print(trip.header.survey_name)
78
+
79
+ # Save to JSON
80
+ CompassInterface.save_json(project, Path("cave.json"))
81
+ """
82
+
83
+ # -------------------------------------------------------------------------
84
+ # Loading Methods (File → Model)
85
+ # -------------------------------------------------------------------------
86
+
87
+ @classmethod
88
+ def load_project(
89
+ cls,
90
+ mak_path: Path,
91
+ *,
92
+ encoding: str = DEFAULT_ENCODING,
93
+ on_progress: ProgressCallback | None = None,
94
+ cancellation: CancellationToken | None = None,
95
+ ) -> CompassMakFile:
96
+ """Load a complete Compass project (MAK + all DAT files).
97
+
98
+ This is the main entry point for loading Compass projects. It:
99
+ 1. Parses the MAK file to dictionary
100
+ 2. Parses each referenced DAT file to dictionary
101
+ 3. Nests DAT dictionaries into file directives
102
+ 4. Validates entire structure with single model_validate() call
103
+
104
+ Args:
105
+ mak_path: Path to the .MAK file
106
+ encoding: Character encoding (default: Windows-1252)
107
+ on_progress: Optional progress callback
108
+ cancellation: Optional cancellation token
109
+
110
+ Returns:
111
+ CompassMakFile with all DAT data loaded
112
+
113
+ Raises:
114
+ InterruptedError: If cancellation was requested
115
+ FileNotFoundError: If MAK file doesn't exist
116
+ """
117
+ if on_progress:
118
+ on_progress(message=f"Reading {mak_path}")
119
+
120
+ # Parse MAK file to dictionary
121
+ mak_parser = CompassProjectParser()
122
+ with mak_path.open(encoding=encoding, errors="replace") as f:
123
+ mak_content = f.read()
124
+ mak_data = mak_parser.parse_string_to_dict(mak_content, str(mak_path))
125
+
126
+ if cancellation and cancellation.cancelled:
127
+ raise InterruptedError("Operation cancelled")
128
+
129
+ # Calculate total size for progress
130
+ mak_dir = mak_path.parent
131
+ total_size = 0
132
+ dat_files: list[tuple[dict[str, Any], Path]] = []
133
+
134
+ for directive in mak_data.get("directives", []):
135
+ if directive.get("type") == "file":
136
+ dat_path = mak_dir / directive["file"]
137
+ if dat_path.exists():
138
+ total_size += dat_path.stat().st_size
139
+ dat_files.append((directive, dat_path))
140
+
141
+ # Parse each DAT file and attach to directive dictionary
142
+ completed = 0
143
+ dat_parser = CompassSurveyParser()
144
+
145
+ for directive, dat_path in dat_files:
146
+ if cancellation and cancellation.cancelled:
147
+ raise InterruptedError("Operation cancelled")
148
+
149
+ if on_progress:
150
+ on_progress(
151
+ message=f"Reading {dat_path.name}",
152
+ completed=completed,
153
+ total=total_size,
154
+ )
155
+
156
+ # Parse DAT file to dictionary and attach
157
+ with dat_path.open(encoding=encoding, errors="replace") as f:
158
+ dat_content = f.read()
159
+ dat_data = dat_parser.parse_string_to_dict(dat_content, str(dat_path))
160
+ directive["data"] = dat_data
161
+
162
+ completed += dat_path.stat().st_size
163
+ if on_progress:
164
+ on_progress(completed=completed, total=total_size)
165
+
166
+ # Single model_validate() call for entire structure
167
+ return CompassMakFile.model_validate(mak_data)
168
+
169
+ @classmethod
170
+ def load_dat(
171
+ cls,
172
+ path: Path,
173
+ *,
174
+ encoding: str = DEFAULT_ENCODING,
175
+ ) -> CompassDatFile:
176
+ """Load a single DAT file.
177
+
178
+ Args:
179
+ path: Path to the .DAT file
180
+ encoding: Character encoding
181
+
182
+ Returns:
183
+ CompassDatFile with all trips
184
+ """
185
+ parser = CompassSurveyParser()
186
+ with path.open(encoding=encoding, errors="replace") as f:
187
+ content = f.read()
188
+ data = parser.parse_string_to_dict(content, str(path))
189
+
190
+ # Single model_validate() call
191
+ return CompassDatFile.model_validate(data)
192
+
193
+ @classmethod
194
+ def load_mak(
195
+ cls,
196
+ path: Path,
197
+ *,
198
+ encoding: str = DEFAULT_ENCODING,
199
+ ) -> CompassMakFile:
200
+ """Load a MAK file without loading DAT files.
201
+
202
+ Args:
203
+ path: Path to the .MAK file
204
+ encoding: Character encoding
205
+
206
+ Returns:
207
+ CompassMakFile with directives only (no DAT data)
208
+ """
209
+ parser = CompassProjectParser()
210
+ with path.open(encoding=encoding, errors="replace") as f:
211
+ content = f.read()
212
+ data = parser.parse_string_to_dict(content, str(path))
213
+
214
+ # Single model_validate() call
215
+ return CompassMakFile.model_validate(data)
216
+
217
+ # -------------------------------------------------------------------------
218
+ # Saving Methods (Model → File)
219
+ # -------------------------------------------------------------------------
220
+
221
+ @classmethod
222
+ def save_project(
223
+ cls,
224
+ project: CompassMakFile,
225
+ mak_path: Path,
226
+ *,
227
+ encoding: str = DEFAULT_ENCODING,
228
+ save_dat_files: bool = True,
229
+ ) -> None:
230
+ """Save a complete Compass project (MAK + all DAT files).
231
+
232
+ Args:
233
+ project: The project to save
234
+ mak_path: Path to write the .MAK file
235
+ encoding: Character encoding (default: Windows-1252)
236
+ save_dat_files: Whether to also save DAT files (default: True)
237
+ """
238
+ # Save MAK file
239
+ cls.save_mak(project, mak_path, encoding=encoding)
240
+
241
+ # Optionally save DAT files
242
+ if save_dat_files:
243
+ mak_dir = mak_path.parent
244
+ for file_dir in project.file_directives:
245
+ if file_dir.data:
246
+ dat_path = mak_dir / file_dir.file
247
+ cls.save_dat(file_dir.data, dat_path, encoding=encoding)
248
+
249
+ @classmethod
250
+ def save_mak(
251
+ cls,
252
+ project: CompassMakFile,
253
+ path: Path,
254
+ *,
255
+ encoding: str = DEFAULT_ENCODING,
256
+ ) -> None:
257
+ """Save a MAK file.
258
+
259
+ Args:
260
+ project: The project to save
261
+ path: Path to write to
262
+ encoding: Character encoding
263
+ """
264
+ content = format_mak_file(project.directives)
265
+ with path.open(mode="w", encoding=encoding, newline="") as f:
266
+ f.write(content or "")
267
+
268
+ @classmethod
269
+ def save_dat(
270
+ cls,
271
+ dat_file: CompassDatFile,
272
+ path: Path,
273
+ *,
274
+ encoding: str = DEFAULT_ENCODING,
275
+ ) -> None:
276
+ """Save a DAT file.
277
+
278
+ Args:
279
+ dat_file: The DAT file to save
280
+ path: Path to write to
281
+ encoding: Character encoding
282
+ """
283
+ content = format_dat_file(dat_file.trips)
284
+ with path.open(mode="w", encoding=encoding, newline="") as f:
285
+ f.write(content or "")
286
+
287
+ # -------------------------------------------------------------------------
288
+ # JSON Methods
289
+ # -------------------------------------------------------------------------
290
+
291
+ @classmethod
292
+ def save_json(
293
+ cls,
294
+ model: CompassMakFile | CompassDatFile,
295
+ path: Path,
296
+ ) -> None:
297
+ """Save a model as JSON.
298
+
299
+ Uses Pydantic's built-in serialization.
300
+
301
+ Args:
302
+ model: Project or DAT file to serialize
303
+ path: Path to write JSON file
304
+ """
305
+ json_str = model.model_dump_json(indent=2, by_alias=True)
306
+ path.write_text(json_str, encoding=JSON_ENCODING)
307
+
308
+ @classmethod
309
+ def load_project_json(cls, path: Path) -> CompassMakFile:
310
+ """Load a project from JSON.
311
+
312
+ Args:
313
+ path: Path to JSON file
314
+
315
+ Returns:
316
+ Deserialized project
317
+ """
318
+ json_str = path.read_text(encoding=JSON_ENCODING)
319
+ return CompassMakFile.model_validate_json(json_str)
320
+
321
+ @classmethod
322
+ def load_dat_json(cls, path: Path) -> CompassDatFile:
323
+ """Load a DAT file from JSON.
324
+
325
+ Args:
326
+ path: Path to JSON file
327
+
328
+ Returns:
329
+ Deserialized DAT file
330
+ """
331
+ json_str = path.read_text(encoding=JSON_ENCODING)
332
+ return CompassDatFile.model_validate_json(json_str)
compass_lib/io.py ADDED
@@ -0,0 +1,246 @@
1
+ # -*- coding: utf-8 -*-
2
+ """File I/O operations for Compass files.
3
+
4
+ This module provides backwards-compatible wrappers around CompassInterface.
5
+
6
+ For new code, prefer using CompassInterface directly:
7
+
8
+ from compass_lib.interface import CompassInterface
9
+
10
+ project = CompassInterface.load_project(Path("cave.mak"))
11
+ CompassInterface.save_json(project, Path("cave.json"))
12
+
13
+ The functions in this module are thin wrappers maintained for API stability.
14
+ """
15
+
16
+ from pathlib import Path
17
+
18
+ from compass_lib.interface import DEFAULT_ENCODING
19
+ from compass_lib.interface import CancellationToken
20
+ from compass_lib.interface import CompassInterface
21
+ from compass_lib.interface import ProgressCallback
22
+ from compass_lib.project.models import CompassMakFile
23
+ from compass_lib.project.models import CompassProjectDirective
24
+ from compass_lib.survey.models import CompassDatFile
25
+ from compass_lib.survey.models import CompassTrip
26
+
27
+ # Re-export for API compatibility
28
+ __all__ = [
29
+ "DEFAULT_ENCODING",
30
+ "CancellationToken",
31
+ "ProgressCallback",
32
+ "load_dat_json",
33
+ "load_project",
34
+ "load_project_json",
35
+ "read_dat_file",
36
+ "read_mak_and_dat_files",
37
+ "read_mak_file",
38
+ "save_dat_json",
39
+ "save_project",
40
+ "save_project_json",
41
+ "write_dat_file",
42
+ "write_mak_file",
43
+ ]
44
+
45
+
46
+ # --- Reading Functions ---
47
+
48
+
49
+ def read_dat_file(
50
+ path: Path,
51
+ *,
52
+ encoding: str = DEFAULT_ENCODING,
53
+ ) -> list[CompassTrip]:
54
+ """Read a Compass .DAT survey file.
55
+
56
+ Args:
57
+ path: Path to the .DAT file
58
+ encoding: Character encoding (default: Windows-1252)
59
+
60
+ Returns:
61
+ List of parsed trips
62
+ """
63
+ dat_file = CompassInterface.load_dat(path, encoding=encoding)
64
+ return dat_file.trips
65
+
66
+
67
+ def read_mak_file(
68
+ path: Path,
69
+ *,
70
+ encoding: str = DEFAULT_ENCODING,
71
+ ) -> list[CompassProjectDirective]:
72
+ """Read a Compass .MAK project file (directives only).
73
+
74
+ This function reads only the MAK file and does not load referenced
75
+ DAT files. Use `load_project()` to load the complete project with
76
+ all DAT file data.
77
+
78
+ Args:
79
+ path: Path to the .MAK file
80
+ encoding: Character encoding (default: Windows-1252)
81
+
82
+ Returns:
83
+ List of parsed directives
84
+ """
85
+ mak_file = CompassInterface.load_mak(path, encoding=encoding)
86
+ return mak_file.directives
87
+
88
+
89
+ def load_project(
90
+ mak_path: Path,
91
+ *,
92
+ encoding: str = DEFAULT_ENCODING,
93
+ on_progress: ProgressCallback | None = None,
94
+ cancellation: CancellationToken | None = None,
95
+ ) -> CompassMakFile:
96
+ """Load a complete Compass project (MAK + all DAT files).
97
+
98
+ This is the main entry point for loading Compass projects.
99
+
100
+ Args:
101
+ mak_path: Path to the .MAK file
102
+ encoding: Character encoding (default: Windows-1252)
103
+ on_progress: Optional progress callback
104
+ cancellation: Optional cancellation token
105
+
106
+ Returns:
107
+ CompassMakFile with all DAT data loaded
108
+
109
+ Raises:
110
+ InterruptedError: If cancellation was requested
111
+ FileNotFoundError: If MAK file doesn't exist
112
+ """
113
+ return CompassInterface.load_project(
114
+ mak_path,
115
+ encoding=encoding,
116
+ on_progress=on_progress,
117
+ cancellation=cancellation,
118
+ )
119
+
120
+
121
+ def read_mak_and_dat_files(
122
+ mak_path: Path,
123
+ *,
124
+ encoding: str = DEFAULT_ENCODING,
125
+ on_progress: ProgressCallback | None = None,
126
+ cancellation: CancellationToken | None = None,
127
+ ) -> list[CompassProjectDirective]:
128
+ """Read a Compass .MAK project file and all linked .DAT files.
129
+
130
+ Note: This function returns a list of directives for backwards compatibility.
131
+ For new code, use `load_project()` which returns a `CompassMakFile` object.
132
+
133
+ Args:
134
+ mak_path: Path to the .MAK file
135
+ encoding: Character encoding (default: Windows-1252)
136
+ on_progress: Optional progress callback
137
+ cancellation: Optional cancellation token
138
+
139
+ Returns:
140
+ List of parsed directives with attached DAT file data
141
+
142
+ Raises:
143
+ InterruptedError: If cancellation was requested
144
+ """
145
+ mak_file = load_project(
146
+ mak_path,
147
+ encoding=encoding,
148
+ on_progress=on_progress,
149
+ cancellation=cancellation,
150
+ )
151
+ return mak_file.directives
152
+
153
+
154
+ # --- Writing Functions ---
155
+
156
+
157
+ def write_dat_file(
158
+ path: Path,
159
+ trips: list[CompassTrip],
160
+ *,
161
+ encoding: str = DEFAULT_ENCODING,
162
+ ) -> None:
163
+ """Write a Compass .DAT survey file.
164
+
165
+ Args:
166
+ path: Path to write to
167
+ trips: List of trips to write
168
+ encoding: Character encoding (default: Windows-1252)
169
+ """
170
+ dat_file = CompassDatFile(trips=trips)
171
+ CompassInterface.save_dat(dat_file, path, encoding=encoding)
172
+
173
+
174
+ def write_mak_file(
175
+ path: Path,
176
+ directives: list[CompassProjectDirective],
177
+ *,
178
+ encoding: str = DEFAULT_ENCODING,
179
+ ) -> None:
180
+ """Write a Compass .MAK project file.
181
+
182
+ Args:
183
+ path: Path to write to
184
+ directives: List of directives to write
185
+ encoding: Character encoding (default: Windows-1252)
186
+ """
187
+ project = CompassMakFile(directives=directives)
188
+ CompassInterface.save_mak(project, path, encoding=encoding)
189
+
190
+
191
+ def save_project(
192
+ mak_path: Path,
193
+ project: CompassMakFile,
194
+ *,
195
+ encoding: str = DEFAULT_ENCODING,
196
+ save_dat_files: bool = True,
197
+ ) -> None:
198
+ """Save a complete Compass project (MAK + all DAT files).
199
+
200
+ Args:
201
+ mak_path: Path to write the .MAK file
202
+ project: The project to save
203
+ encoding: Character encoding (default: Windows-1252)
204
+ save_dat_files: Whether to also save DAT files (default: True)
205
+ """
206
+ CompassInterface.save_project(
207
+ project,
208
+ mak_path,
209
+ encoding=encoding,
210
+ save_dat_files=save_dat_files,
211
+ )
212
+
213
+
214
+ # --- JSON I/O Functions ---
215
+
216
+
217
+ def save_project_json(path: Path, project: CompassMakFile) -> None:
218
+ """Save a project as JSON.
219
+
220
+ Args:
221
+ path: Path to write JSON file
222
+ project: Project to serialize
223
+ """
224
+ CompassInterface.save_json(project, path)
225
+
226
+
227
+ def load_project_json(path: Path) -> CompassMakFile:
228
+ """Load a project from JSON.
229
+
230
+ Args:
231
+ path: Path to JSON file
232
+
233
+ Returns:
234
+ Deserialized project
235
+ """
236
+ return CompassInterface.load_project_json(path)
237
+
238
+
239
+ def save_dat_json(path: Path, dat_file: CompassDatFile) -> None:
240
+ """Save a DAT file as JSON."""
241
+ CompassInterface.save_json(dat_file, path)
242
+
243
+
244
+ def load_dat_json(path: Path) -> CompassDatFile:
245
+ """Load a DAT file from JSON."""
246
+ return CompassInterface.load_dat_json(path)