compass-lib 0.0.1__py3-none-any.whl → 0.0.2__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.
compass_lib/__init__.py CHANGED
@@ -1,5 +1,7 @@
1
- #!/usr/bin/env python
1
+ # -*- coding: utf-8 -*-
2
2
 
3
- import importlib.metadata
3
+ """
4
+ "A library to read Compass Survey files"
5
+ """
4
6
 
5
- __version__ = importlib.metadata.version("compass_lib")
7
+ __version__ = "0.0.2"
@@ -1 +0,0 @@
1
- #!/usr/bin/env python
@@ -1,4 +1,4 @@
1
- # #!/usr/bin/env python3
1
+ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  from pathlib import Path
@@ -10,22 +10,25 @@ def convert(args: list[str]) -> int:
10
10
  parser = argparse.ArgumentParser(prog="compass convert")
11
11
 
12
12
  parser.add_argument(
13
+ "-i",
13
14
  "--input_file",
14
15
  type=str,
15
16
  default=None,
16
17
  required=True,
17
- help="Compass Survey Source File."
18
+ help="Compass Survey Source File.",
18
19
  )
19
20
 
20
21
  parser.add_argument(
22
+ "-o",
21
23
  "--output_file",
22
24
  type=str,
23
25
  default=None,
24
26
  required=True,
25
- help="Path to save the converted file at."
27
+ help="Path to save the converted file at.",
26
28
  )
27
29
 
28
30
  parser.add_argument(
31
+ "-w",
29
32
  "--overwrite",
30
33
  action="store_true",
31
34
  help="Allow overwrite an already existing file.",
@@ -33,11 +36,12 @@ def convert(args: list[str]) -> int:
33
36
  )
34
37
 
35
38
  parser.add_argument(
39
+ "-f",
36
40
  "--format",
37
41
  type=str,
38
42
  choices=["json"],
39
43
  required=True,
40
- help="Conversion format used."
44
+ help="Conversion format used.",
41
45
  )
42
46
 
43
47
  parsed_args = parser.parse_args(args)
@@ -48,9 +52,11 @@ def convert(args: list[str]) -> int:
48
52
 
49
53
  output_file = Path(parsed_args.output_file)
50
54
  if output_file.exists() and not parsed_args.overwrite:
51
- raise FileExistsError(f"The file {output_file} already existing. "
52
- "Please pass the flag `--overwrite` to ignore.")
55
+ raise FileExistsError(
56
+ f"The file {output_file} already existing. "
57
+ "Please pass the flag `--overwrite` to ignore."
58
+ )
53
59
 
54
- parser = CompassParser(dmp_file)
55
- parser.to_json(filepath=output_file)
60
+ survey = CompassParser.load_dat_file(dmp_file)
61
+ survey.to_json(filepath=output_file)
56
62
  return 0
@@ -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="compass 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
@@ -1,15 +1,17 @@
1
- # #!/usr/bin/env python3
1
+ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  from importlib.metadata import entry_points
5
5
 
6
6
  import compass_lib
7
7
 
8
+
8
9
  def main():
9
- registered_commands = entry_points(group='compass.actions')
10
+ registered_commands = entry_points(group="compass.actions")
10
11
 
11
12
  parser = argparse.ArgumentParser(prog="compass_lib")
12
13
  parser.add_argument(
14
+ "-v",
13
15
  "--version",
14
16
  action="version",
15
17
  version=f"%(prog)s version: {compass_lib.__version__}",
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from compass_lib.enums import ShotFlag
6
+
7
+ # ============================== SPECIAL CHARS ============================== #
8
+
9
+ COMPASS_SECTION_SEPARATOR = "\f" # Form_feed: https://www.ascii-code.com/12
10
+ COMPASS_END_OF_FILE = "\x1a" # Substitute: https://www.ascii-code.com/26
11
+
12
+ # ================================== REGEX ================================== #
13
+ # Priorized regex:
14
+ # 1. Section Split with `\r\n`
15
+ # 2. Section Split with `\n`
16
+ # 3. Section Split with `\f` alone
17
+ COMPASS_SECTION_SPLIT_RE = re.compile(
18
+ rf"{COMPASS_SECTION_SEPARATOR}\r\n|{COMPASS_SECTION_SEPARATOR}\n|{COMPASS_SECTION_SEPARATOR}"
19
+ )
20
+
21
+ # String format:
22
+ # - `SURVEY NAME: toc+187?`
23
+ COMPASS_SECTION_NAME_RE = re.compile(r"SURVEY NAME:\s*(?P<section_name>\S*)")
24
+
25
+ # String format:
26
+ # - `SURVEY DATE: 1 26 86`
27
+ # - `SURVEY DATE: 4 22 2001`
28
+ # - `SURVEY DATE: 8 28 1988 COMMENT:Surface to shelter`
29
+ COMPASS_DATE_COMMENT_RE = re.compile(
30
+ r"^SURVEY DATE:\s*(?P<date>\d{1,2}\s+\d{1,2}\s+\d{2,4}|None)(?:\s+COMMENT:\s*(?P<comment>.*))?$" # noqa: E501
31
+ )
32
+
33
+ COMPASS_SHOT_FLAGS_RE = re.compile(
34
+ rf"({ShotFlag.__start_token__}"
35
+ rf"([{''.join(ShotFlag._value2member_map_.keys())}]*){ShotFlag.__end_token__})*(.*)"
36
+ )
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import json
5
+ import uuid
6
+ from dataclasses import asdict
7
+ from dataclasses import is_dataclass
8
+
9
+
10
+ class EnhancedJSONEncoder(json.JSONEncoder):
11
+ def default(self, obj):
12
+ from compass_lib.parser import ShotFlag # noqa: PLC0415
13
+
14
+ match obj:
15
+ case datetime.date():
16
+ return obj.isoformat()
17
+
18
+ case ShotFlag():
19
+ return obj.value
20
+
21
+ case uuid.UUID():
22
+ return str(obj)
23
+
24
+ if is_dataclass(obj):
25
+ return asdict(obj)
26
+
27
+ return super().default(obj)
compass_lib/enums.py CHANGED
@@ -1,10 +1,40 @@
1
+ from __future__ import annotations
2
+
1
3
  from enum import Enum
4
+ from enum import IntEnum
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from typing_extensions import Self
10
+
2
11
 
3
12
  class CustomEnum(Enum):
4
13
  @classmethod
5
14
  def reverse(cls, name):
6
15
  return cls._value2member_map_[name]
7
16
 
17
+
18
+ class CompassFileType(IntEnum):
19
+ DAT = 0
20
+ MAK = 1
21
+ PLT = 2
22
+
23
+ @classmethod
24
+ def from_str(cls, value: str) -> Self:
25
+ try:
26
+ return cls[value.upper()]
27
+ except KeyError as e:
28
+ raise ValueError(f"Unknown value: {value.upper()}") from e
29
+
30
+ @classmethod
31
+ def from_path(cls, filepath: str | Path):
32
+ if not isinstance(filepath, Path):
33
+ filepath = Path(filepath)
34
+
35
+ return cls.from_str(filepath.suffix.upper()[1:]) # Remove the leading `.`
36
+
37
+
8
38
  # ============================== Azimuth ============================== #
9
39
 
10
40
  # export const azimuthUnits: { [string]: DisplayAzimuthUnit } = {
@@ -13,11 +43,13 @@ class CustomEnum(Enum):
13
43
  # G: 'gradians',
14
44
  # }
15
45
 
46
+
16
47
  class AzimuthUnits(CustomEnum):
17
48
  DEGREES = "D"
18
49
  QUADS = "Q"
19
50
  GRADIANS = "G"
20
51
 
52
+
21
53
  # ============================== Inclination Unit ============================== #
22
54
 
23
55
  # export const inclinationUnits: { [string]: DisplayInclinationUnit } = {
@@ -28,6 +60,7 @@ class AzimuthUnits(CustomEnum):
28
60
  # W: 'depthGauge',
29
61
  # }
30
62
 
63
+
31
64
  class InclinationUnits(CustomEnum):
32
65
  DEGREES = "D"
33
66
  PERCENT_GRADE = "G"
@@ -35,6 +68,7 @@ class InclinationUnits(CustomEnum):
35
68
  GRADIANS = "R"
36
69
  DEPTH_GAUGE = "W"
37
70
 
71
+
38
72
  # ============================== Length Unit ============================== #
39
73
 
40
74
 
@@ -44,11 +78,13 @@ class InclinationUnits(CustomEnum):
44
78
  # M: 'meters',
45
79
  # }
46
80
 
81
+
47
82
  class LengthUnits(CustomEnum):
48
83
  DECIMAL_FEET = "D"
49
84
  FEET_AND_INCHES = "I"
50
85
  METERS = "M"
51
86
 
87
+
52
88
  # ============================== LRUD ============================== #
53
89
 
54
90
  # export const lrudItems: { [string]: LrudItem } = {
@@ -58,12 +94,14 @@ class LengthUnits(CustomEnum):
58
94
  # D: 'down',
59
95
  # }
60
96
 
97
+
61
98
  class LRUD(CustomEnum):
62
99
  LEFT = "L"
63
100
  RIGHT = "R"
64
101
  UP = "U"
65
102
  DOWN = "D"
66
103
 
104
+
67
105
  # ============================== ShotItem ============================== #
68
106
 
69
107
  # export const shotMeasurementItems: { [string]: ShotMeasurementItem } = {
@@ -74,6 +112,7 @@ class LRUD(CustomEnum):
74
112
  # d: 'backsightInclination',
75
113
  # }
76
114
 
115
+
77
116
  class ShotItem(CustomEnum):
78
117
  LENGTH = "L"
79
118
  FRONTSIGHT_AZIMUTH = "A"
@@ -81,6 +120,7 @@ class ShotItem(CustomEnum):
81
120
  BACKSIGHT_AZIMUTH = "a"
82
121
  BACKSIGHT_INCLINATION = "d"
83
122
 
123
+
84
124
  # ============================== StationSide ============================== #
85
125
 
86
126
  # export const stationSides: { [string]: StationSide } = {
@@ -88,6 +128,7 @@ class ShotItem(CustomEnum):
88
128
  # T: 'to',
89
129
  # }
90
130
 
131
+
91
132
  class StationSide(CustomEnum):
92
133
  FROM = "F"
93
134
  TO = "T"
@@ -95,6 +136,7 @@ class StationSide(CustomEnum):
95
136
 
96
137
  # ============================== ShotFlag ============================== #
97
138
 
139
+
98
140
  class ShotFlag(CustomEnum):
99
141
  EXCLUDE_PLOTING = "P"
100
142
  EXCLUDE_CLOSURE = "C"
@@ -102,11 +144,5 @@ class ShotFlag(CustomEnum):
102
144
  TOTAL_EXCLUSION = "X"
103
145
  SPLAY = "S"
104
146
 
105
- __start_token__ = r"#\|"
106
- __end_token__ = r"#"
107
-
108
-
109
-
110
- if __name__ == "__main__":
111
- print(StationSide.FROM.value)
112
- print(StationSide.reverse("F"))
147
+ __start_token__ = r"#\|" # noqa: S105
148
+ __end_token__ = r"#" # noqa: S105
compass_lib/models.py ADDED
@@ -0,0 +1,129 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime # noqa: TC003
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel
10
+ from pydantic import ConfigDict
11
+ from pydantic import Field
12
+ from pydantic import field_validator
13
+
14
+ from compass_lib.encoding import EnhancedJSONEncoder
15
+
16
+ # from compass_lib.errors import DuplicateValueError
17
+
18
+
19
+ class SurveyShot(BaseModel):
20
+ from_id: str
21
+ to_id: str
22
+
23
+ azimuth: Annotated[float, Field(ge=0, lt=360)]
24
+
25
+ inclination: Annotated[float, Field(ge=-90, le=90)]
26
+ length: Annotated[float, Field(ge=0)]
27
+
28
+ # Optional Values
29
+ comment: str | None = None
30
+ flags: Any | None = None
31
+
32
+ azimuth2: Annotated[float, Field(ge=0, lt=360)] | None = None
33
+ inclination2: Annotated[float, Field(ge=-90, le=90)] | None = None
34
+
35
+ # LRUD
36
+ left: Annotated[float, Field(ge=0)] = 0.0
37
+ right: Annotated[float, Field(ge=0)] = 0.0
38
+ up: Annotated[float, Field(ge=0)] = 0.0
39
+ down: Annotated[float, Field(ge=0)] = 0.0
40
+
41
+ model_config = ConfigDict(extra="forbid")
42
+
43
+ @field_validator("left", "right", "up", "down", mode="before")
44
+ @classmethod
45
+ def validate_lrud(cls, value: float) -> float:
46
+ return value if value > 0 else 0.0
47
+
48
+ @field_validator("azimuth", "azimuth2", mode="before")
49
+ @classmethod
50
+ def validate_azimuth(cls, value: float) -> float:
51
+ return value if value > 0 else 0.0
52
+
53
+ @field_validator("inclination2", mode="before")
54
+ @classmethod
55
+ def validate_inclination2(cls, value: float) -> float:
56
+ return value if -90 <= value <= 90 else 0.0
57
+
58
+ # ======================== VALIDATOR UTILS ======================== #
59
+
60
+ # @classmethod
61
+ # def validate_unique(cls, field: str, values: list) -> list:
62
+ # vals2check = [getattr(val, field) for val in values]
63
+ # dupl_vals = list(duplicates(vals2check))
64
+ # if dupl_vals:
65
+ # raise DuplicateValueError(
66
+ # f"[{cls.__name__}] Duplicate value found for `{field}`: "
67
+ # f"{dupl_vals}"
68
+ # )
69
+ # return values
70
+
71
+ # @field_validator("to_id", mode="before")
72
+ # @classmethod
73
+ # def validate_unique_to_id(cls, value: str | None) -> str:
74
+ # """Note: Validators are only ran with custom fed values.
75
+ # Not autogenerated ones. Hence we need to register the name."""
76
+
77
+ # if value is None or value == "":
78
+ # return cls.to_id.default_factory()
79
+
80
+ # # 1. Verify the name is only composed of valid chars.
81
+ # for char in value:
82
+ # if char.upper() not in [
83
+ # *UniqueNameGenerator.VOCAB,
84
+ # *list("_-~:!?.'()[]{}@*&#%|$")
85
+ # ]:
86
+ # raise ValueError(f"The character `{char}` is not allowed as `name`.")
87
+
88
+ # if len(value) > COMPASS_MAX_NAME_LENGTH:
89
+ # raise ValueError(f"Name {value} is too long, maximum allowed: "
90
+ # f"{COMPASS_MAX_NAME_LENGTH}")
91
+
92
+ # UniqueNameGenerator.register(value=value)
93
+ # return value
94
+
95
+
96
+ class SurveySection(BaseModel):
97
+ name: str
98
+ comment: str
99
+ correction: list[float]
100
+ correction2: list[float]
101
+ survey_date: datetime.date | None = None
102
+ discovery_date: datetime.date | None = None
103
+ declination: float
104
+ format: str = "DDDDUDLRLADN"
105
+ shots: list[SurveyShot]
106
+ surveyors: str | None = None
107
+
108
+ model_config = ConfigDict(extra="forbid")
109
+
110
+
111
+ class Survey(BaseModel):
112
+ cave_name: str
113
+ description: str = ""
114
+
115
+ sections: list[SurveySection] = []
116
+
117
+ model_config = ConfigDict(extra="forbid")
118
+
119
+ def to_json(self, filepath: str | Path | None = None) -> str:
120
+ filepath = Path(filepath) if filepath else None
121
+ data = self.model_dump()
122
+
123
+ json_str = json.dumps(data, indent=4, sort_keys=True, cls=EnhancedJSONEncoder)
124
+
125
+ if filepath is not None:
126
+ with filepath.open(mode="w") as file:
127
+ file.write(json_str)
128
+
129
+ return json_str