gyomu-schema 0.1.1__tar.gz

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 (37) hide show
  1. gyomu_schema-0.1.1/.gitignore +19 -0
  2. gyomu_schema-0.1.1/PKG-INFO +12 -0
  3. gyomu_schema-0.1.1/README.md +3 -0
  4. gyomu_schema-0.1.1/pyproject.toml +17 -0
  5. gyomu_schema-0.1.1/src/gyomu_schema/__init__.py +0 -0
  6. gyomu_schema-0.1.1/src/gyomu_schema/config/config_loader_option.py +35 -0
  7. gyomu_schema-0.1.1/src/gyomu_schema/convert.py +38 -0
  8. gyomu_schema-0.1.1/src/gyomu_schema/db/config.py +5 -0
  9. gyomu_schema-0.1.1/src/gyomu_schema/error/__init__.py +16 -0
  10. gyomu_schema-0.1.1/src/gyomu_schema/error/base.py +56 -0
  11. gyomu_schema-0.1.1/src/gyomu_schema/error/config.py +30 -0
  12. gyomu_schema-0.1.1/src/gyomu_schema/error/database.py +5 -0
  13. gyomu_schema-0.1.1/src/gyomu_schema/error/gyomu.py +38 -0
  14. gyomu_schema-0.1.1/src/gyomu_schema/error/io.py +5 -0
  15. gyomu_schema-0.1.1/src/gyomu_schema/error/timeout.py +28 -0
  16. gyomu_schema-0.1.1/src/gyomu_schema/error/validation.py +24 -0
  17. gyomu_schema-0.1.1/src/gyomu_schema/filesystem/__init__.py +0 -0
  18. gyomu_schema-0.1.1/src/gyomu_schema/filesystem/file.py +213 -0
  19. gyomu_schema-0.1.1/src/gyomu_schema/gyomu/holiday/business_calendar.py +68 -0
  20. gyomu_schema-0.1.1/src/gyomu_schema/gyomu/parameter/parameter_access.py +94 -0
  21. gyomu_schema-0.1.1/src/gyomu_schema/gyomu/user.py +6 -0
  22. gyomu_schema-0.1.1/src/gyomu_schema/gyomu/variable/variable_translator.py +114 -0
  23. gyomu_schema-0.1.1/src/gyomu_schema/logger/config.py +21 -0
  24. gyomu_schema-0.1.1/src/gyomu_schema/logger/logger.py +46 -0
  25. gyomu_schema-0.1.1/src/gyomu_schema/market_holiday.py +10 -0
  26. gyomu_schema-0.1.1/src/gyomu_schema/parameter/__init__.py +0 -0
  27. gyomu_schema-0.1.1/src/gyomu_schema/parameter/parameter_master.py +26 -0
  28. gyomu_schema-0.1.1/src/gyomu_schema/py.typed +0 -0
  29. gyomu_schema-0.1.1/src/gyomu_schema/utility/polling.py +47 -0
  30. gyomu_schema-0.1.1/tests/unit/conversion/test_convert.py +121 -0
  31. gyomu_schema-0.1.1/tests/unit/error/test_base.py +38 -0
  32. gyomu_schema-0.1.1/tests/unit/error/test_errors.py +47 -0
  33. gyomu_schema-0.1.1/tests/unit/error/test_gyomu.py +46 -0
  34. gyomu_schema-0.1.1/tests/unit/filesystem/test_file.py +594 -0
  35. gyomu_schema-0.1.1/tests/unit/gyomu/holiday/test_market_holiday.py +15 -0
  36. gyomu_schema-0.1.1/tests/unit/logger/test_logger_config.py +90 -0
  37. gyomu_schema-0.1.1/tests/unit/utility/test_polling.py +83 -0
@@ -0,0 +1,19 @@
1
+ venv/
2
+ .idea/
3
+
4
+ /build/
5
+ /dist/
6
+ /src/gyomu.egg-info/
7
+ /.coverage
8
+ /htmlcov/
9
+ __pycache__/
10
+ *.py[cod]
11
+
12
+ .env
13
+ .env.*
14
+ !.env.example
15
+
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ htmlcov/
19
+ *.egg-info/
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.5
2
+ Name: gyomu-schema
3
+ Version: 0.1.1
4
+ Summary: Gyomu canonical schemas
5
+ Requires-Python: >=3.14
6
+ Requires-Dist: pydantic<3,>=2
7
+ Requires-Dist: returns>=0.29.0
8
+ Description-Content-Type: text/markdown
9
+
10
+ # gyomu-schema
11
+
12
+ Canonical schemas and common types for Gyomu Python.
@@ -0,0 +1,3 @@
1
+ # gyomu-schema
2
+
3
+ Canonical schemas and common types for Gyomu Python.
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "gyomu-schema"
3
+ dynamic = ["version"]
4
+ description = "Gyomu canonical schemas"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "pydantic>=2,<3",
9
+ "returns>=0.29.0",
10
+ ]
11
+
12
+ [build-system]
13
+ requires = ["hatchling"]
14
+ build-backend = "hatchling.build"
15
+
16
+ [tool.hatch.version]
17
+ path = "../../version/__about__.py"
File without changes
@@ -0,0 +1,35 @@
1
+ from collections.abc import Mapping
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class EnvironmentLoaderOption:
8
+ variables: Mapping[str, str]
9
+ use_dot_env: bool = False
10
+ dot_env_path: Path | None = None
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class BaseFileLoaderOption:
15
+ file_path: Path
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class JsonLoaderOption(BaseFileLoaderOption):
20
+ pass
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class YamlLoaderOption(BaseFileLoaderOption):
25
+ pass
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class TomlLoaderOption(BaseFileLoaderOption):
30
+ pass
31
+
32
+
33
+ type ConfigLoaderOption = (
34
+ EnvironmentLoaderOption | JsonLoaderOption | YamlLoaderOption | TomlLoaderOption
35
+ )
@@ -0,0 +1,38 @@
1
+ import json
2
+
3
+ from pydantic import BaseModel
4
+ from pydantic import ValidationError as PydanticValidationError
5
+ from returns.result import Failure, Result, Success
6
+
7
+ from gyomu_schema.error.io import GyomuIOError
8
+ from gyomu_schema.error.validation import ValidationError
9
+
10
+
11
+ def convert[T: BaseModel](
12
+ schema: type[T],
13
+ value: object,
14
+ ) -> Result[T, ValidationError]:
15
+ try:
16
+ return Success(schema.model_validate(value))
17
+ except PydanticValidationError as exc:
18
+ return Failure(
19
+ ValidationError(
20
+ message="Schema validation failed",
21
+ input=value,
22
+ details={
23
+ "issues": exc.errors(),
24
+ },
25
+ ).chain(exc)
26
+ )
27
+
28
+
29
+ def convert_json[T: BaseModel](
30
+ schema: type[T],
31
+ content: str,
32
+ ) -> Result[T, GyomuIOError | ValidationError]:
33
+ try:
34
+ value = json.loads(content)
35
+ except json.JSONDecodeError as exc:
36
+ return Failure(GyomuIOError("Fail to load JSON").chain(exc))
37
+
38
+ return convert(schema, value)
@@ -0,0 +1,5 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class DbConfig(BaseModel):
5
+ connection_string: str
@@ -0,0 +1,16 @@
1
+ from gyomu_schema.error.base import BaseError
2
+ from gyomu_schema.error.config import ConfigError
3
+ from gyomu_schema.error.database import DatabaseError
4
+ from gyomu_schema.error.gyomu import GyomuError, GyomuErrorReason
5
+ from gyomu_schema.error.io import GyomuIOError
6
+ from gyomu_schema.error.validation import ValidationError
7
+
8
+ __all__ = [
9
+ "BaseError",
10
+ "ConfigError",
11
+ "DatabaseError",
12
+ "GyomuError",
13
+ "GyomuErrorReason",
14
+ "GyomuIOError",
15
+ "ValidationError",
16
+ ]
@@ -0,0 +1,56 @@
1
+ from collections.abc import Mapping
2
+ from typing import Self
3
+
4
+
5
+ class BaseError(Exception):
6
+ """Base class for Gyomu application errors.
7
+
8
+ Provides common contextual information for errors in addition to the
9
+ information already provided by Python's :class:`Exception`.
10
+
11
+ Args:
12
+ message: A human-readable description of the error.
13
+ context: An error context identifier.
14
+ details: Additional structured diagnostic information.
15
+
16
+ Attributes:
17
+ context: The error context identifier.
18
+ details: Additional structured diagnostic information.
19
+
20
+ Gyomu Context:
21
+ context identifies the operation or location where the error occurred.
22
+ It describes what the application was doing when the error occurred
23
+ and does not replace the Python traceback.
24
+
25
+ details contains additional structured information useful for
26
+ diagnosing the error, such as relevant input values, configuration
27
+ metadata, or other operational information.
28
+
29
+ The original cause of an error is represented by Python's exception
30
+ chaining mechanism (__cause__). The :meth:`chain` method can be used
31
+ to explicitly associate an underlying exception as the cause.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ message: str,
37
+ *,
38
+ context: str | None = None,
39
+ details: Mapping[str, object] | None = None,
40
+ ) -> None:
41
+ """Initialize a BaseError."""
42
+ super().__init__(message)
43
+ self.context = context
44
+ self.details = details
45
+
46
+ def chain(self, cause: BaseException) -> Self:
47
+ """Associate an exception as the explicit cause of this error.
48
+
49
+ Args:
50
+ cause: The underlying exception that caused this error.
51
+
52
+ Returns:
53
+ This error instance.
54
+ """
55
+ self.__cause__ = cause
56
+ return self
@@ -0,0 +1,30 @@
1
+ from collections.abc import Mapping
2
+ from typing import Literal
3
+
4
+ from gyomu_schema.error.base import BaseError
5
+
6
+ type ConfigPhase = Literal["load", "parse", "decode", "validate"]
7
+ type ConfigSource = Literal["env", "yaml", "json", "toml"]
8
+
9
+
10
+ class ConfigError(BaseError):
11
+ """Configuration error."""
12
+
13
+ def __init__(
14
+ self,
15
+ message: str,
16
+ *,
17
+ source: ConfigSource,
18
+ schema: object,
19
+ phase: ConfigPhase,
20
+ context: str | None = None,
21
+ details: Mapping[str, object] | None = None,
22
+ ) -> None:
23
+ super().__init__(
24
+ message,
25
+ context=context,
26
+ details=details,
27
+ )
28
+ self.source = source
29
+ self.schema = schema
30
+ self.phase = phase
@@ -0,0 +1,5 @@
1
+ from gyomu_schema.error.io import GyomuIOError
2
+
3
+
4
+ class DatabaseError(GyomuIOError):
5
+ """Database access error."""
@@ -0,0 +1,38 @@
1
+ from collections.abc import Mapping
2
+ from typing import Literal
3
+
4
+ from gyomu_schema.error.base import BaseError
5
+
6
+ GyomuErrorReason = Literal[
7
+ "invalid_input",
8
+ "not_found",
9
+ "external_failure",
10
+ "unexpected",
11
+ "concurrent_modification",
12
+ "out_of_bounds",
13
+ ]
14
+
15
+
16
+ class GyomuError(BaseError):
17
+ """Application-level Gyomu error."""
18
+
19
+ def __init__(
20
+ self,
21
+ message: str,
22
+ *,
23
+ operation: str,
24
+ domain: str,
25
+ reason: GyomuErrorReason,
26
+ retryable: bool = False,
27
+ context: str | None = None,
28
+ details: Mapping[str, object] | None = None,
29
+ ) -> None:
30
+ super().__init__(
31
+ message,
32
+ context=context,
33
+ details=details,
34
+ )
35
+ self.operation = operation
36
+ self.domain = domain
37
+ self.reason = reason
38
+ self.retryable = retryable
@@ -0,0 +1,5 @@
1
+ from gyomu_schema.error.base import BaseError
2
+
3
+
4
+ class GyomuIOError(BaseError):
5
+ """I/O related error."""
@@ -0,0 +1,28 @@
1
+ from collections.abc import Mapping
2
+
3
+ from gyomu_schema.error.base import BaseError
4
+
5
+
6
+ class GyomuTimeoutError(BaseError):
7
+ """Timeout related error."""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ *,
13
+ action: str,
14
+ timeout_seconds: float,
15
+ interval_seconds: float | None = None,
16
+ elapsed_seconds: float | None = None,
17
+ context: str | None = None,
18
+ details: Mapping[str, object] | None = None,
19
+ ) -> None:
20
+ super().__init__(
21
+ message,
22
+ context=context,
23
+ details=details,
24
+ )
25
+ self.action = action
26
+ self.timeout_seconds = timeout_seconds
27
+ self.interval_seconds = interval_seconds
28
+ self.elapsed_seconds = elapsed_seconds
@@ -0,0 +1,24 @@
1
+ from collections.abc import Mapping
2
+
3
+ from gyomu_schema.error.base import BaseError
4
+
5
+
6
+ class ValidationError(BaseError):
7
+ """Validation error."""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ *,
13
+ location: tuple[str | int, ...] | None = None,
14
+ input: object | None = None,
15
+ context: str | None = None,
16
+ details: Mapping[str, object] | None = None,
17
+ ) -> None:
18
+ super().__init__(
19
+ message,
20
+ context=context,
21
+ details=details,
22
+ )
23
+ self.location = location
24
+ self.input = input
@@ -0,0 +1,213 @@
1
+ from datetime import UTC, datetime
2
+ from enum import Enum
3
+ from os import path
4
+ from pathlib import Path
5
+
6
+
7
+ class FileInfo:
8
+ @staticmethod
9
+ def epoch_to_datetimeutc(epoch: float) -> datetime:
10
+ return datetime.fromtimestamp(epoch, tz=UTC)
11
+
12
+ file_name: str
13
+ full_path: Path
14
+ dir_name: str
15
+ dir_path: Path
16
+ size: int
17
+ extension: str
18
+ create_time_utc: datetime
19
+ update_time_utc: datetime
20
+ last_access_time_utc: datetime
21
+
22
+ def __init__(self, file_path: Path):
23
+ p = file_path
24
+ self.file_name = p.name
25
+ self.full_path = p.resolve()
26
+ self.dir_name = p.parent.name
27
+ self.dir_path = p.parent
28
+ self.size = path.getsize(file_path)
29
+ self.extension = "".join(p.suffix)
30
+ self.create_time_utc = FileInfo.epoch_to_datetimeutc(path.getctime(p))
31
+ self.update_time_utc = FileInfo.epoch_to_datetimeutc(path.getmtime(p))
32
+ self.last_access_time_utc = FileInfo.epoch_to_datetimeutc(path.getatime(p))
33
+
34
+
35
+ class FileFilterType(Enum):
36
+ FILE_NAME = "file_name"
37
+ CREATE_TIME_UTC = "create_time_utc"
38
+ LAST_ACCESS_TIME_UTC = "last_access_time_utc"
39
+ LAST_WRITE_TIME_UTC = "last_write_time_utc"
40
+
41
+
42
+ class FileCompareType(Enum):
43
+ EQUAL = "equal"
44
+ LARGER = "larger"
45
+ LESS = "less"
46
+ LARGER_OR_EQUAL = "larger_or_equal"
47
+ LESS_OR_EQUAL = "less_or_equal"
48
+
49
+
50
+ class FileFilterInfo:
51
+ name_filter: str
52
+ target_date: datetime
53
+
54
+ def __init__(
55
+ self,
56
+ kind: FileFilterType,
57
+ operator: FileCompareType,
58
+ value: object,
59
+ ):
60
+ self.kind = kind
61
+ self.operator = operator
62
+
63
+ if self.kind == FileFilterType.FILE_NAME:
64
+ self.name_filter = str(value)
65
+ elif isinstance(value, datetime):
66
+ self.target_date = value
67
+ elif isinstance(value, str):
68
+ try:
69
+ self.target_date = datetime.strptime(value, "%Y%m%d")
70
+ except ValueError as e:
71
+ raise ValueError(f"Date Parameter is invalid: {value}") from e
72
+ else:
73
+ raise ValueError(f"Date Parameter is invalid: {value}")
74
+
75
+
76
+ class FileTransportInfo:
77
+ """
78
+ Base Sdir Sname Ddir Dname (S)full+base (S)Full (S)path (S)name (D)full (D)path (D)name
79
+ x x x x x base/SDir/Sname SDir/Sname SDir Sname Ddir/Dname Ddir Dname
80
+ x x x x base/SDir/Sname SDir/Sname SDir Sname Ddir/Sname Ddir Sname
81
+ x x x x base/SDir/Sname SDir/Sname SDir Sname SDir/Dname Sdir Dname
82
+ x x x base/SDir/Sname SDir/Sname SDir Sname SDir/Sname SDir Sname
83
+ x x x base/SDir SDir SDir Ddir Ddir
84
+ x x base/SDir SDir SDir SDir SDir
85
+ x base
86
+ x x base Ddir Ddir
87
+ x x x x base/Sname Sname Sname Ddir/Dname Ddir Dname
88
+ x x x base/Sname Sname Sname Ddir/Sname Ddir Sname
89
+ x x x base/Sname Sname Sname Dname Dname
90
+ x x base/Sname Sname Sname Sname Sname
91
+ x x x x SDir/Sname SDir Sname Ddir/Dname Ddir Dname
92
+ x x x SDir/Sname SDir Sname Ddir/Sname Ddir Sname
93
+ x x x SDir/Sname SDir Sname SDir/Dname SDir Dname
94
+ x x SDir/Sname SDir Sname SDir/Sname SDir Sname
95
+ x x SDir SDir Ddir Ddir
96
+ x SDir SDir SDir SDir
97
+ x x x Sname Sname Ddir/Dname Ddir Dname
98
+ x x Sname Sname Ddir/Sname Ddir Sname
99
+ x x Sname Sname Dname Dname
100
+ x Sname Sname Sname Sname
101
+ """ # noqa: E501
102
+
103
+ __source_filename: str
104
+ __source_folder_name: str
105
+ __base_path: str
106
+ __destination_filename: str
107
+ __destination_folder_name: str
108
+ delete_sourcefile_after_completion: bool = False
109
+ overwrite_destination: bool = False
110
+ filter_conditions: list[FileFilterInfo] | None = None
111
+
112
+ @property
113
+ def is_source_directory(self) -> bool:
114
+ return not self.source_filename
115
+
116
+ @property
117
+ def is_destination_directory(self) -> bool:
118
+ return not self.destination_filename
119
+
120
+ @property
121
+ def is_destination_root(self) -> bool:
122
+ return bool(
123
+ not self.__source_folder_name and not self.__destination_folder_name
124
+ )
125
+
126
+ @property
127
+ def source_fullname(self) -> str:
128
+ if not self.__source_folder_name:
129
+ return self.__source_filename
130
+ if not self.__source_filename:
131
+ return self.__source_folder_name
132
+ return path.join(self.__source_folder_name, self.__source_filename)
133
+
134
+ @property
135
+ def source_fullname_with_basepath(self) -> str:
136
+ if not self.source_fullname:
137
+ return self.__base_path
138
+ return (
139
+ self.source_fullname
140
+ if not self.__base_path
141
+ else path.join(self.__base_path, self.source_fullname)
142
+ )
143
+
144
+ @property
145
+ def source_path(self) -> str:
146
+ return self.__source_folder_name
147
+
148
+ @property
149
+ def source_filename(self) -> str:
150
+ return self.__source_filename
151
+
152
+ @property
153
+ def destination_filename(self) -> str:
154
+ return (
155
+ self.__source_filename
156
+ if not self.__destination_filename
157
+ else self.__destination_filename
158
+ )
159
+
160
+ @property
161
+ def destination_path(self) -> str:
162
+ return (
163
+ self.__source_folder_name
164
+ if not self.__destination_folder_name
165
+ else self.__destination_folder_name
166
+ )
167
+
168
+ @property
169
+ def destination_fullname(self) -> str:
170
+ if not self.destination_path:
171
+ return self.destination_filename
172
+ if not self.destination_filename:
173
+ return self.destination_path
174
+ return path.join(self.destination_path, self.destination_filename)
175
+
176
+ def __init__(
177
+ self,
178
+ base_path: str = "",
179
+ source_filename: str = "",
180
+ source_folder_name: str = "",
181
+ destination_filename: str = "",
182
+ destination_foldername: str = "",
183
+ delete_sourcefile_after_completion: bool = False,
184
+ overwrite_destination: bool = False,
185
+ filter_conditions: list[FileFilterInfo] | None = None,
186
+ ):
187
+ self.__base_path = base_path
188
+ self.__source_filename = source_filename
189
+ self.__source_folder_name = source_folder_name
190
+ self.__destination_filename = destination_filename
191
+ self.__destination_folder_name = destination_foldername
192
+ self.delete_sourcefile_after_completion = delete_sourcefile_after_completion
193
+ self.overwrite_destination = overwrite_destination
194
+ self.filter_conditions = filter_conditions
195
+
196
+ if not self.__source_filename and self.__destination_filename:
197
+ raise ValueError("Invalid Parameter")
198
+
199
+ if (
200
+ not self.__base_path
201
+ and not self.__source_folder_name
202
+ and not self.__source_filename
203
+ ):
204
+ raise ValueError("Invalid Parameter")
205
+
206
+
207
+ class FileArchiveType(Enum):
208
+ ZIP = ("zip",)
209
+ TGZ = ("tgz",)
210
+ BZIP2 = ("bz2",)
211
+ GZIP = ("gz",)
212
+ TAR = ("tar",)
213
+ GuessFromFileName = "unknown"
@@ -0,0 +1,68 @@
1
+ from datetime import date
2
+ from typing import Protocol
3
+
4
+
5
+ class BusinessCalendar(Protocol):
6
+ def is_business_day(
7
+ self,
8
+ target_date: date,
9
+ ) -> bool: ...
10
+
11
+ def business_day(
12
+ self,
13
+ target_date: date,
14
+ day_offset: int,
15
+ ) -> date: ...
16
+
17
+ def get_holidays(
18
+ self,
19
+ start_date: date,
20
+ end_date: date,
21
+ ) -> list[date]: ...
22
+
23
+ def business_day_of_beginning_month_with_offset(
24
+ self,
25
+ target_date: date,
26
+ day_offset: int = 1,
27
+ ) -> date: ...
28
+
29
+ def business_day_of_beginning_of_next_month_with_offset(
30
+ self,
31
+ target_date: date,
32
+ day_offset: int = 1,
33
+ ) -> date: ...
34
+
35
+ def business_day_of_beginning_of_previous_month_with_offset(
36
+ self,
37
+ target_date: date,
38
+ day_offset: int = 1,
39
+ ) -> date: ...
40
+ def business_day_of_end_month_with_offset(
41
+ self,
42
+ target_date: date,
43
+ day_offset: int,
44
+ ) -> date: ...
45
+
46
+ def business_day_of_end_of_next_month_with_offset(
47
+ self,
48
+ target_date: date,
49
+ day_offset: int,
50
+ ) -> date: ...
51
+
52
+ def business_day_of_end_of_previous_month_with_offset(
53
+ self,
54
+ target_date: date,
55
+ day_offset: int,
56
+ ) -> date: ...
57
+
58
+ def business_day_of_beginning_of_year(
59
+ self,
60
+ target_date: date,
61
+ day_offset: int,
62
+ ) -> date: ...
63
+
64
+ def business_day_of_end_of_year(
65
+ self,
66
+ target_date: date,
67
+ day_offset: int,
68
+ ) -> date: ...