mreg-api 0.1.0__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.
- mreg_api/__about__.py +7 -0
- mreg_api/__init__.py +25 -0
- mreg_api/_version.py +34 -0
- mreg_api/cache.py +205 -0
- mreg_api/client.py +989 -0
- mreg_api/dirs.py +23 -0
- mreg_api/endpoints.py +152 -0
- mreg_api/exceptions.py +375 -0
- mreg_api/models/__init__.py +99 -0
- mreg_api/models/abstracts.py +536 -0
- mreg_api/models/fields.py +178 -0
- mreg_api/models/history.py +101 -0
- mreg_api/models/models.py +3849 -0
- mreg_api/py.typed +0 -0
- mreg_api/types.py +86 -0
- mreg_api/utilities/__init__.py +5 -0
- mreg_api/utilities/fs.py +84 -0
- mreg_api/utilities/shared.py +42 -0
- mreg_api-0.1.0.dist-info/METADATA +726 -0
- mreg_api-0.1.0.dist-info/RECORD +23 -0
- mreg_api-0.1.0.dist-info/WHEEL +5 -0
- mreg_api-0.1.0.dist-info/licenses/LICENSE +674 -0
- mreg_api-0.1.0.dist-info/top_level.txt +1 -0
mreg_api/__about__.py
ADDED
mreg_api/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""MREG API - Python client library for MREG."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from mreg_api import cache
|
|
6
|
+
from mreg_api import dirs
|
|
7
|
+
from mreg_api import endpoints
|
|
8
|
+
from mreg_api import exceptions
|
|
9
|
+
from mreg_api import models
|
|
10
|
+
from mreg_api import types
|
|
11
|
+
from mreg_api.__about__ import __version__
|
|
12
|
+
from mreg_api.cache import CacheConfig
|
|
13
|
+
from mreg_api.client import MregClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"__version__",
|
|
17
|
+
"MregClient",
|
|
18
|
+
"CacheConfig",
|
|
19
|
+
"cache",
|
|
20
|
+
"dirs",
|
|
21
|
+
"endpoints",
|
|
22
|
+
"exceptions",
|
|
23
|
+
"models",
|
|
24
|
+
"types",
|
|
25
|
+
]
|
mreg_api/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
mreg_api/cache.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Caching utilities for mreg-api."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Generic
|
|
7
|
+
from typing import ParamSpec
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
from typing import cast
|
|
10
|
+
from typing import final
|
|
11
|
+
|
|
12
|
+
from diskcache import Cache
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from pydantic import ByteSize
|
|
15
|
+
from pydantic import field_serializer
|
|
16
|
+
|
|
17
|
+
from mreg_api.exceptions import CacheError
|
|
18
|
+
from mreg_api.exceptions import CacheMiss
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
P = ParamSpec("P")
|
|
24
|
+
T = TypeVar("T")
|
|
25
|
+
|
|
26
|
+
DEFAULT_CACHE_TAG = "mreg-api"
|
|
27
|
+
DEFAULT_CACHE_TTL = 300 # seconds
|
|
28
|
+
|
|
29
|
+
_CACHE_MISS = object()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CacheInfo(BaseModel):
|
|
33
|
+
"""Information about the cache."""
|
|
34
|
+
|
|
35
|
+
items: int
|
|
36
|
+
size: ByteSize
|
|
37
|
+
hits: int
|
|
38
|
+
misses: int
|
|
39
|
+
ttl: int
|
|
40
|
+
directory: str
|
|
41
|
+
|
|
42
|
+
@field_serializer("size")
|
|
43
|
+
def _serialize_size(self, value: ByteSize) -> str:
|
|
44
|
+
return value.human_readable()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _create_cache(config: CacheConfig) -> Cache | None:
|
|
48
|
+
"""Create a diskcache.Cache based on the provided configuration.
|
|
49
|
+
|
|
50
|
+
If the diskcache.Cache cannot be created (e.g., filesystem access denied),
|
|
51
|
+
returns None.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
config: Cache configuration.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
diskcache.Cache instance, or None if creation failed.
|
|
58
|
+
"""
|
|
59
|
+
if not config.enable:
|
|
60
|
+
logger.debug("Cache is disabled in config, not creating cache.")
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
return Cache(directory=config.directory, timeout=config.timeout)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.warning("Failed to create diskcache.Cache: %s. Cache will be disabled.", e)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class CacheConfig(BaseModel):
|
|
71
|
+
"""Configuration for the mreg-api cache."""
|
|
72
|
+
|
|
73
|
+
enable: bool = True
|
|
74
|
+
ttl: int = 300
|
|
75
|
+
tag: str = "mreg-api"
|
|
76
|
+
timeout: int = 60
|
|
77
|
+
directory: str | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@final
|
|
81
|
+
class MregApiCache(Generic[T]):
|
|
82
|
+
"""Wrapper around the mreg-api cache.
|
|
83
|
+
|
|
84
|
+
This class can operate in two modes:
|
|
85
|
+
1. Enabled: When a valid diskcache.Cache is provided
|
|
86
|
+
2. Disabled: When cache is None (no-op mode, all operations are safe to call)
|
|
87
|
+
|
|
88
|
+
The disabled mode allows callers to use the cache interface without null checks.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, cache: Cache | None, config: CacheConfig) -> None:
|
|
92
|
+
"""Initialize the cache wrapper.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
cache: The underlying diskcache.Cache instance, or None for disabled mode.
|
|
96
|
+
config: Configuration for cache behavior.
|
|
97
|
+
"""
|
|
98
|
+
# NOTE: VERY IMPORTANT!!
|
|
99
|
+
# _NEVER_ do `if self._cache` checks, as diskcache.Cache implements __bool__
|
|
100
|
+
# to check if the cache is non-empty, which is NOT what we want anywhere.
|
|
101
|
+
self._cache = cache
|
|
102
|
+
self.config = config
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def has_backend(self) -> bool:
|
|
106
|
+
"""Return True if an underlying cache backend is available."""
|
|
107
|
+
return self._cache is not None
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def is_enabled(self) -> bool:
|
|
111
|
+
"""Return True if cache is operational (has backend and is enabled in config)."""
|
|
112
|
+
return self.has_backend and self.config.enable
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def new(cls, config: CacheConfig) -> MregApiCache[T]:
|
|
116
|
+
"""Create a new MregApiCache instance based on the provided configuration.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
config: Cache configuration.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
MregApiCache instance (may be in disabled mode if creation failed).
|
|
123
|
+
"""
|
|
124
|
+
cache = _create_cache(config)
|
|
125
|
+
return cls(cache, config=config)
|
|
126
|
+
|
|
127
|
+
def enable(self) -> None:
|
|
128
|
+
"""Enable the cache."""
|
|
129
|
+
self.config.enable = True
|
|
130
|
+
# We have no cache backend yet, try to create one
|
|
131
|
+
if self._cache is None:
|
|
132
|
+
self._cache = _create_cache(self.config)
|
|
133
|
+
|
|
134
|
+
def disable(self) -> None:
|
|
135
|
+
"""Disable the cache."""
|
|
136
|
+
self.config.enable = False
|
|
137
|
+
|
|
138
|
+
def get_info(self) -> CacheInfo | None:
|
|
139
|
+
"""Get information about the cache.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
CacheInfo object with cache statistics, or None if cache is disabled.
|
|
143
|
+
"""
|
|
144
|
+
# No cache object exists.
|
|
145
|
+
# We can return stats for disabled cache as long as we have a backend.
|
|
146
|
+
if self._cache is None:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
hits, misses = self._cache.stats()
|
|
150
|
+
|
|
151
|
+
return CacheInfo(
|
|
152
|
+
size=self._cache.volume(), # pyright: ignore[reportAny]
|
|
153
|
+
hits=hits, # pyright: ignore[reportArgumentType]
|
|
154
|
+
misses=misses, # pyright: ignore[reportArgumentType]
|
|
155
|
+
items=len(self._cache), # pyright: ignore[reportArgumentType]
|
|
156
|
+
directory=self._cache.directory,
|
|
157
|
+
ttl=self.config.ttl,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def set(self, key: str, value: T | None, expire: int | None = None) -> None:
|
|
161
|
+
"""Set a value in the cache.
|
|
162
|
+
|
|
163
|
+
No-op if cache is disabled.
|
|
164
|
+
"""
|
|
165
|
+
if not self.is_enabled or self._cache is None:
|
|
166
|
+
return
|
|
167
|
+
try:
|
|
168
|
+
self._cache.set(key, value, expire=expire or self.config.ttl, tag=self.config.tag)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
raise CacheError(f"Failed to set cache key {key}: {e}") from e
|
|
171
|
+
|
|
172
|
+
def get(self, key: str) -> T | None:
|
|
173
|
+
"""Get a value from the cache.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
CacheMiss: If the key is not found in the cache or cache is disabled.
|
|
177
|
+
"""
|
|
178
|
+
if not self.is_enabled or self._cache is None:
|
|
179
|
+
raise CacheMiss(f"Cache disabled, key: {key}")
|
|
180
|
+
|
|
181
|
+
# Use sentinel to distinguish between None and missing key
|
|
182
|
+
try:
|
|
183
|
+
value = self._cache.get(key, default=_CACHE_MISS) # pyright: ignore[reportReturnType, reportUnknownVariableType]
|
|
184
|
+
except Exception as e:
|
|
185
|
+
raise CacheError(f"Failed to get cache key {key}: {e}") from e
|
|
186
|
+
|
|
187
|
+
if value is _CACHE_MISS:
|
|
188
|
+
raise CacheMiss(f"Cache miss for key: {key}")
|
|
189
|
+
# NOTE: diskcache.Cache.get has poor type annotations
|
|
190
|
+
# so we cannot properly type hint the return value here.
|
|
191
|
+
return cast(T | None, value)
|
|
192
|
+
|
|
193
|
+
def clear(self) -> int:
|
|
194
|
+
"""Clear the cache and reset statistics.
|
|
195
|
+
|
|
196
|
+
Returns 0 if cache is disabled.
|
|
197
|
+
"""
|
|
198
|
+
if not self.is_enabled or self._cache is None:
|
|
199
|
+
return 0
|
|
200
|
+
try:
|
|
201
|
+
n_items = self._cache.evict(self.config.tag)
|
|
202
|
+
logger.info("Cleared %d items from cache with tag %s", n_items, self.config.tag)
|
|
203
|
+
return n_items
|
|
204
|
+
except Exception as e:
|
|
205
|
+
raise CacheError(f"Failed to clear cache: {e}") from e
|