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.
- openspeleo_lib/__init__.py +10 -0
- openspeleo_lib/commands/__init__.py +0 -0
- openspeleo_lib/commands/convert.py +103 -0
- openspeleo_lib/commands/encrypt.py +89 -0
- openspeleo_lib/commands/main.py +33 -0
- openspeleo_lib/commands/validate_tml.py +34 -0
- openspeleo_lib/constants.py +17 -0
- openspeleo_lib/debug_utils.py +17 -0
- openspeleo_lib/enums.py +28 -0
- openspeleo_lib/errors.py +9 -0
- openspeleo_lib/generators.py +93 -0
- openspeleo_lib/geo_utils.py +57 -0
- openspeleo_lib/geojson.py +264 -0
- openspeleo_lib/interfaces/__init__.py +5 -0
- openspeleo_lib/interfaces/ariane/__init__.py +0 -0
- openspeleo_lib/interfaces/ariane/decoding.py +141 -0
- openspeleo_lib/interfaces/ariane/encoding.py +67 -0
- openspeleo_lib/interfaces/ariane/enums_cls.py +53 -0
- openspeleo_lib/interfaces/ariane/interface.py +115 -0
- openspeleo_lib/interfaces/ariane/name_map.py +64 -0
- openspeleo_lib/interfaces/ariane/xml_utils.py +18 -0
- openspeleo_lib/interfaces/base.py +37 -0
- openspeleo_lib/logger.py +119 -0
- openspeleo_lib/models.py +388 -0
- openspeleo_lib/pydantic_utils.py +48 -0
- openspeleo_lib/utils.py +33 -0
- openspeleo_lib-0.0.12.dist-info/METADATA +49 -0
- openspeleo_lib-0.0.12.dist-info/RECORD +31 -0
- openspeleo_lib-0.0.12.dist-info/WHEEL +4 -0
- openspeleo_lib-0.0.12.dist-info/entry_points.txt +8 -0
- openspeleo_lib-0.0.12.dist-info/licenses/LICENSE +201 -0
|
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
|
+
)
|
openspeleo_lib/enums.py
ADDED
|
@@ -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"
|
openspeleo_lib/errors.py
ADDED
|
@@ -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}°")
|