openspeleo-lib 0.0.12__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.
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ "A library to read/write and convert OpenSpeleo files"
5
+ """
6
+
7
+ __version__ = "0.0.12"
8
+
9
+ # Initialize the logger
10
+ from openspeleo_lib import logger # noqa: F401 # pyright: ignore[reportUnusedImport]
File without changes
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import pathlib
6
+
7
+ import orjson
8
+
9
+ from openspeleo_lib.geojson import survey_to_geojson
10
+ from openspeleo_lib.interfaces import ArianeInterface
11
+
12
+ logger = logging.getLogger(__name__)
13
+ logger.setLevel(logging.INFO)
14
+
15
+
16
+ def convert(args):
17
+ parser = argparse.ArgumentParser(
18
+ prog="convert", description="Convert a Survey File"
19
+ )
20
+ parser.add_argument(
21
+ "-i",
22
+ "--input_file",
23
+ type=pathlib.Path,
24
+ required=True,
25
+ help="Path to the TML file to be validated",
26
+ )
27
+
28
+ parser.add_argument(
29
+ "-o",
30
+ "--output_file",
31
+ type=pathlib.Path,
32
+ default=None,
33
+ required=True,
34
+ help="Path to save the converted file at.",
35
+ )
36
+
37
+ parser.add_argument(
38
+ "-w",
39
+ "--overwrite",
40
+ action="store_true",
41
+ help="Allow overwrite an already existing file.",
42
+ default=False,
43
+ )
44
+
45
+ parser.add_argument(
46
+ "-b",
47
+ "--beautify",
48
+ action="store_true",
49
+ help="Beautify the JSON output (indent=2 and sorted).",
50
+ default=False,
51
+ )
52
+
53
+ parser.add_argument(
54
+ "-f",
55
+ "--format",
56
+ type=str,
57
+ choices=["geojson", "json"],
58
+ required=True,
59
+ help="Conversion format used.",
60
+ )
61
+
62
+ parsed_args = parser.parse_args(args)
63
+
64
+ input_file: pathlib.Path = parsed_args.input_file
65
+ output_file: pathlib.Path = parsed_args.output_file
66
+
67
+ if not input_file.exists():
68
+ raise FileNotFoundError(f"File not found: `{input_file}`")
69
+
70
+ if output_file.exists() and not parsed_args.overwrite:
71
+ raise FileExistsError(
72
+ f"The file `{output_file}` already existing. "
73
+ "Please pass the flag `--overwrite` to ignore."
74
+ )
75
+
76
+ match input_file.suffix:
77
+ case ".tml":
78
+ survey = ArianeInterface.from_file(input_file)
79
+
80
+ case _:
81
+ raise ValueError(f"Unsupported file format: `{input_file.suffix}`")
82
+
83
+ match parsed_args.format:
84
+ case "geojson":
85
+ geojson_data = survey_to_geojson(survey)
86
+ with output_file.open(mode="wb") as f:
87
+ f.write(
88
+ orjson.dumps(
89
+ geojson_data,
90
+ None,
91
+ option=(
92
+ (orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS)
93
+ if parsed_args.beautify
94
+ else None
95
+ ),
96
+ )
97
+ )
98
+
99
+ case "json":
100
+ survey.to_json(filepath=output_file, beautify=parsed_args.beautify)
101
+
102
+ case _:
103
+ raise ValueError(f"Unsupported conversion format: `{parsed_args.format}`")
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from cryptography.fernet import Fernet
9
+ from dotenv import load_dotenv
10
+
11
+ logger = logging.getLogger(__name__)
12
+ logger.setLevel(logging.INFO)
13
+
14
+
15
+ def encrypt(args: list[str]) -> int:
16
+ parser = argparse.ArgumentParser(prog="openspeleo encrypt")
17
+
18
+ parser.add_argument(
19
+ "-i",
20
+ "--input_file",
21
+ type=str,
22
+ default=None,
23
+ required=True,
24
+ help="Compass Survey Source File.",
25
+ )
26
+
27
+ parser.add_argument(
28
+ "-o",
29
+ "--output_file",
30
+ type=str,
31
+ default=None,
32
+ required=True,
33
+ help="Path to save the converted file at.",
34
+ )
35
+
36
+ parser.add_argument(
37
+ "-e",
38
+ "--env_file",
39
+ type=str,
40
+ default=None,
41
+ required=True,
42
+ help="Path of the environment file containing the Fernet key.",
43
+ )
44
+
45
+ parser.add_argument(
46
+ "-w",
47
+ "--overwrite",
48
+ action="store_true",
49
+ help="Allow overwrite an already existing file.",
50
+ default=False,
51
+ )
52
+
53
+ parsed_args = parser.parse_args(args)
54
+
55
+ if not (input_file := Path(parsed_args.input_file)).exists():
56
+ raise FileNotFoundError(f"Impossible to find: `{input_file}`.")
57
+
58
+ if (
59
+ output_file := Path(parsed_args.output_file)
60
+ ).exists() and not parsed_args.overwrite:
61
+ raise FileExistsError(
62
+ f"The file {output_file} already existing. "
63
+ "Please pass the flag `--overwrite` to ignore."
64
+ )
65
+
66
+ if not (envfile := Path(parsed_args.env_file)).exists():
67
+ raise FileNotFoundError(f"Impossible to find: `{envfile}`.")
68
+ load_dotenv(envfile, verbose=True, override=True)
69
+ logger.info("Loaded environment variables from: `%s`", envfile)
70
+
71
+ if (fernet_key := os.getenv("ARTIFACT_ENCRYPTION_KEY")) is None:
72
+ raise ValueError(
73
+ "No Fernet key found in the environment file. "
74
+ "Check if `ARTIFACT_ENCRYPTION_KEY` is set."
75
+ )
76
+ fernet_key = Fernet(fernet_key)
77
+
78
+ with input_file.open("rb") as f:
79
+ clear_data = f.read()
80
+
81
+ with output_file.open("wb") as f:
82
+ f.write(fernet_key.encrypt(clear_data))
83
+
84
+ # Round Trip Check:
85
+ with output_file.open("rb") as f:
86
+ roundtrip_data = fernet_key.decrypt(f.read())
87
+ assert clear_data == roundtrip_data
88
+
89
+ return 0
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from importlib.metadata import entry_points
5
+
6
+ import openspeleo_lib
7
+
8
+
9
+ def main():
10
+ registered_commands = entry_points(group="openspeleo_lib.actions")
11
+
12
+ parser = argparse.ArgumentParser(prog="openspeleo_lib")
13
+ parser.add_argument(
14
+ "-v",
15
+ "--version",
16
+ action="version",
17
+ version=f"%(prog)s version: {openspeleo_lib.__version__}",
18
+ )
19
+ parser.add_argument(
20
+ "command",
21
+ choices=registered_commands.names,
22
+ )
23
+ parser.add_argument(
24
+ "args",
25
+ help=argparse.SUPPRESS,
26
+ nargs=argparse.REMAINDER,
27
+ )
28
+
29
+ args = argparse.Namespace()
30
+ parser.parse_args(namespace=args)
31
+
32
+ main_fn = registered_commands[args.command].load()
33
+ return main_fn(args.args)
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import pathlib
6
+
7
+ from openspeleo_lib.interfaces import ArianeInterface
8
+
9
+ logger = logging.getLogger(__name__)
10
+ logger.setLevel(logging.INFO)
11
+
12
+
13
+ def validate(args):
14
+ parser = argparse.ArgumentParser(
15
+ prog="validate_tml", description="Validate a TML file"
16
+ )
17
+ parser.add_argument(
18
+ "-i",
19
+ "--input_file",
20
+ type=pathlib.Path,
21
+ required=True,
22
+ help="Path to the TML file to be validated",
23
+ )
24
+
25
+ parsed_args = parser.parse_args(args)
26
+
27
+ input_file = parsed_args.input_file
28
+
29
+ if not input_file.exists():
30
+ raise FileNotFoundError(f"File not found: `{input_file}`")
31
+
32
+ _ = ArianeInterface.from_file(input_file)
33
+
34
+ logger.info("Filepath: `%(input_file)s` ... VALID", {"input_file": input_file})
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ # Length for the shot field `name` used by compass
4
+ OSPL_SHOTNAME_MAX_LENGTH = 50
5
+ OSPL_SHOTNAME_DEFAULT_LENGTH = 6
6
+
7
+ # Length for the section field `name`
8
+ OSPL_SECTIONNAME_MAX_LENGTH = 500
9
+
10
+ # Maximum retry attempts for any while loop
11
+ OSPL_MAX_RETRY_ATTEMPTS = 100
12
+
13
+ # Filenames
14
+ ARIANE_DATA_FILENAME = "Data.xml"
15
+
16
+ # GEOJSON DIGIT PRECISION
17
+ OSPL_GEOJSON_DIGIT_PRECISION = 7
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import orjson
6
+
7
+ if TYPE_CHECKING:
8
+ from pathlib import Path
9
+
10
+
11
+ def write_debugdata_to_disk(data: dict, filepath: Path) -> None:
12
+ with filepath.open(mode="w") as f:
13
+ f.write(
14
+ orjson.dumps(
15
+ data, None, option=(orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS)
16
+ ).decode("utf-8")
17
+ )
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class CustomEnum(Enum):
7
+ @classmethod
8
+ def reverse(cls, name):
9
+ return cls._value2member_map_[name]
10
+
11
+
12
+ class ArianeProfileType(CustomEnum):
13
+ VERTICAL = "VERTICAL"
14
+ HORIZONTAL = "HORIZONTAL"
15
+ PERPENDICULAR = "PERPENDICULAR"
16
+ BISECTION = "BISECTION"
17
+
18
+
19
+ class ArianeShotType(CustomEnum):
20
+ REAL = "REAL"
21
+ START = "START"
22
+ VIRTUAL = "VIRTUAL"
23
+ CLOSURE = "CLOSURE"
24
+
25
+
26
+ class LengthUnits(CustomEnum):
27
+ FEET = "FT"
28
+ METERS = "M"
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class DuplicateValueError(ValueError):
5
+ pass
6
+
7
+
8
+ class MaxRetriesError(RuntimeError):
9
+ pass
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import random
5
+ from collections import defaultdict
6
+ from typing import Any
7
+ from typing import NewType
8
+
9
+ from openspeleo_lib.constants import OSPL_MAX_RETRY_ATTEMPTS
10
+ from openspeleo_lib.constants import OSPL_SHOTNAME_DEFAULT_LENGTH
11
+ from openspeleo_lib.constants import OSPL_SHOTNAME_MAX_LENGTH
12
+ from openspeleo_lib.errors import DuplicateValueError
13
+ from openspeleo_lib.errors import MaxRetriesError
14
+
15
+
16
+ class UniqueValueGenerator:
17
+ _used_values = None
18
+ VOCAB = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
19
+
20
+ def __init__(self):
21
+ raise NotImplementedError("This class should not be instantiated.")
22
+
23
+ @classmethod
24
+ @contextlib.contextmanager
25
+ def activate_uniqueness(cls):
26
+ try:
27
+ cls._used_values = defaultdict(set)
28
+ yield
29
+ finally:
30
+ cls._used_values = None
31
+
32
+ @classmethod
33
+ def register(cls, vartype: type, value: Any) -> None:
34
+ """Register the generated value."""
35
+ if cls._used_values is None: # uniqueness is not activated
36
+ return
37
+
38
+ value = vartype(value)
39
+
40
+ if value in cls._used_values[vartype]:
41
+ raise DuplicateValueError(
42
+ f"Value `{value}` for type `{vartype}` has already been registered."
43
+ )
44
+
45
+ cls._used_values[vartype].add(value)
46
+
47
+ @classmethod
48
+ def get(cls, vartype: type, **kwargs) -> Any:
49
+ """Get unique value for an object primary key."""
50
+ iter_idx = 0
51
+ while True:
52
+ iter_idx += 1
53
+ if iter_idx > OSPL_MAX_RETRY_ATTEMPTS:
54
+ raise MaxRetriesError(
55
+ "Impossible to find an available value to use. "
56
+ "Max retry attempts reached: "
57
+ f"{OSPL_MAX_RETRY_ATTEMPTS}"
58
+ )
59
+ try:
60
+ if vartype is str or (
61
+ isinstance(vartype, NewType) and vartype.__supertype__ is str
62
+ ):
63
+ value = cls._generate_str(**kwargs)
64
+
65
+ elif vartype is int or (
66
+ isinstance(vartype, NewType) and vartype.__supertype__ is int
67
+ ):
68
+ value = cls._generate_int(
69
+ known_values=(
70
+ cls._used_values[vartype] if cls._used_values else []
71
+ )
72
+ )
73
+ else:
74
+ raise TypeError(f"Unsupported type: `{vartype}`")
75
+
76
+ cls.register(vartype=vartype, value=value)
77
+ break
78
+ except DuplicateValueError:
79
+ continue
80
+
81
+ return value
82
+
83
+ @classmethod
84
+ def _generate_str(cls, str_len: int = OSPL_SHOTNAME_DEFAULT_LENGTH) -> str:
85
+ if str_len > OSPL_SHOTNAME_MAX_LENGTH:
86
+ raise ValueError(
87
+ f"Maximum length allowed: {OSPL_SHOTNAME_MAX_LENGTH}, received: {str_len}" # noqa: E501
88
+ )
89
+ return "".join(random.choices(cls.VOCAB, k=str_len))
90
+
91
+ @classmethod
92
+ def _generate_int(cls, known_values: list[int] | set[int]) -> str:
93
+ return max(known_values, default=0) + 1
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+
5
+ import pyIGRF14 as pyIGRF
6
+ from pydantic import BaseModel
7
+ from pydantic_extra_types.coordinate import Latitude # noqa: TC002
8
+ from pydantic_extra_types.coordinate import Longitude # noqa: TC002
9
+
10
+ from openspeleo_lib.constants import OSPL_GEOJSON_DIGIT_PRECISION
11
+
12
+ # ruff: noqa: T201
13
+
14
+
15
+ class GeoLocation(BaseModel):
16
+ latitude: Latitude
17
+ longitude: Longitude
18
+
19
+ def as_tuple(self) -> tuple[float, float]:
20
+ """Return the latitude and longitude as a tuple.
21
+ # RFC 7946: (longitude, latitude)
22
+ """
23
+ return (
24
+ round(self.longitude, OSPL_GEOJSON_DIGIT_PRECISION),
25
+ round(self.latitude, OSPL_GEOJSON_DIGIT_PRECISION),
26
+ )
27
+
28
+
29
+ def decimal_year(dt: datetime.datetime) -> float:
30
+ dt_start = datetime.datetime(
31
+ year=dt.year, month=1, day=1, hour=0, minute=0, second=0
32
+ )
33
+ dt_end = datetime.datetime(
34
+ year=dt.year + 1, month=1, day=1, hour=0, minute=0, second=0
35
+ )
36
+ return round(
37
+ dt.year + (dt - dt_start).total_seconds() / (dt_end - dt_start).total_seconds(),
38
+ ndigits=2,
39
+ )
40
+
41
+
42
+ def get_declination(location: GeoLocation, dt: datetime.datetime) -> float:
43
+ declination, _, _, _, _, _, _ = pyIGRF.igrf_value(
44
+ location.latitude,
45
+ location.longitude,
46
+ alt=0.0,
47
+ year=decimal_year(dt),
48
+ )
49
+ return round(declination, 2)
50
+
51
+
52
+ if __name__ == "__main__":
53
+ dt = datetime.datetime(2025, 7, 1)
54
+
55
+ d1 = get_declination(GeoLocation(latitude=20.6296, longitude=-87.0739), dt)
56
+
57
+ print(f"pyIGRF declination : {d1:.6f}°")