python-corekit 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.
Files changed (125) hide show
  1. corekit/__init__.py +0 -0
  2. corekit/api/__init__.py +9 -0
  3. corekit/api/handler.py +76 -0
  4. corekit/api/responses.py +40 -0
  5. corekit/api/routers.py +115 -0
  6. corekit/concurrency/__init__.py +9 -0
  7. corekit/concurrency/decorators.py +72 -0
  8. corekit/concurrency/thread_local.py +99 -0
  9. corekit/concurrency/worker.py +65 -0
  10. corekit/config/__init__.py +47 -0
  11. corekit/config/loader.py +153 -0
  12. corekit/config/settings.py +161 -0
  13. corekit/config/sources.py +125 -0
  14. corekit/connections/__init__.py +31 -0
  15. corekit/connections/connectable.py +212 -0
  16. corekit/connections/decorators.py +92 -0
  17. corekit/connections/redis/__init__.py +7 -0
  18. corekit/connections/redis/connection.py +239 -0
  19. corekit/connections/registry.py +80 -0
  20. corekit/connections/sql/__init__.py +10 -0
  21. corekit/connections/sql/connection.py +342 -0
  22. corekit/connections/sql/fields/__init__.py +7 -0
  23. corekit/connections/sql/fields/jsonb.py +67 -0
  24. corekit/connections/sql/migration/__init__.py +57 -0
  25. corekit/connections/sql/migration/base.py +40 -0
  26. corekit/connections/sql/migration/operations.py +416 -0
  27. corekit/connections/sql/migration/registry.py +166 -0
  28. corekit/connections/sql/migration/table.py +27 -0
  29. corekit/connections/sql/query.py +68 -0
  30. corekit/connections/sql/table.py +96 -0
  31. corekit/constants.py +45 -0
  32. corekit/crypto/__init__.py +1 -0
  33. corekit/crypto/constants.py +7 -0
  34. corekit/crypto/enum.py +11 -0
  35. corekit/crypto/hasher.py +89 -0
  36. corekit/data/__init__.py +81 -0
  37. corekit/data/dataset.py +340 -0
  38. corekit/data/expressions/__init__.py +46 -0
  39. corekit/data/expressions/comparison.py +252 -0
  40. corekit/data/expressions/expression.py +98 -0
  41. corekit/data/record.py +147 -0
  42. corekit/data/stats.py +157 -0
  43. corekit/decorators/__init__.py +2 -0
  44. corekit/decorators/exception_handling.py +43 -0
  45. corekit/decorators/warnings.py +35 -0
  46. corekit/docker/__init__.py +7 -0
  47. corekit/docker/watchdog.py +222 -0
  48. corekit/etl/__init__.py +44 -0
  49. corekit/etl/connection.py +44 -0
  50. corekit/etl/extract/__init__.py +0 -0
  51. corekit/etl/extract/extractor.py +48 -0
  52. corekit/etl/extract/schemas.py +18 -0
  53. corekit/etl/load/__init__.py +0 -0
  54. corekit/etl/load/loader.py +53 -0
  55. corekit/etl/load/schemas.py +33 -0
  56. corekit/etl/orchestrator.py +201 -0
  57. corekit/etl/schemas.py +22 -0
  58. corekit/etl/transform/__init__.py +0 -0
  59. corekit/etl/transform/schemas.py +15 -0
  60. corekit/etl/transform/transformer.py +28 -0
  61. corekit/events/__init__.py +38 -0
  62. corekit/events/enum.py +58 -0
  63. corekit/events/frames.py +51 -0
  64. corekit/events/models.py +23 -0
  65. corekit/events/publisher.py +75 -0
  66. corekit/events/reader.py +132 -0
  67. corekit/events/sse.py +109 -0
  68. corekit/events/websocket.py +97 -0
  69. corekit/exceptions/__init__.py +0 -0
  70. corekit/exceptions/base.py +45 -0
  71. corekit/exceptions/custom/__init__.py +0 -0
  72. corekit/exceptions/http/__init__.py +0 -0
  73. corekit/exceptions/http/exceptions.py +37 -0
  74. corekit/exceptions/types.py +17 -0
  75. corekit/files/__init__.py +25 -0
  76. corekit/files/base.py +117 -0
  77. corekit/files/enum.py +30 -0
  78. corekit/files/json.py +12 -0
  79. corekit/files/pickle.py +12 -0
  80. corekit/files/toml.py +43 -0
  81. corekit/http/__init__.py +0 -0
  82. corekit/http/client.py +176 -0
  83. corekit/http/exponential_backoff.py +100 -0
  84. corekit/http/response.py +12 -0
  85. corekit/log_monitor/__init__.py +23 -0
  86. corekit/log_monitor/constants.py +8 -0
  87. corekit/log_monitor/models.py +150 -0
  88. corekit/log_monitor/service.py +418 -0
  89. corekit/notifications/__init__.py +8 -0
  90. corekit/notifications/base.py +51 -0
  91. corekit/notifications/models.py +34 -0
  92. corekit/observability/__init__.py +21 -0
  93. corekit/observability/benchmarkable.py +12 -0
  94. corekit/observability/loggable.py +29 -0
  95. corekit/observability/timing/__init__.py +0 -0
  96. corekit/observability/timing/constants.py +1 -0
  97. corekit/observability/timing/split.py +20 -0
  98. corekit/observability/timing/timer.py +30 -0
  99. corekit/py.typed +0 -0
  100. corekit/registry/__init__.py +12 -0
  101. corekit/registry/registry.py +134 -0
  102. corekit/schemas/__init__.py +0 -0
  103. corekit/schemas/dataclasses/__init__.py +0 -0
  104. corekit/schemas/enum.py +49 -0
  105. corekit/schemas/models/__init__.py +0 -0
  106. corekit/schemas/models/arbitrary.py +11 -0
  107. corekit/schemas/models/date_models.py +18 -0
  108. corekit/schemas/pydantic/__init__.py +0 -0
  109. corekit/schemas/pydantic/fields.py +35 -0
  110. corekit/schemas/types.py +40 -0
  111. corekit/serialization/__init__.py +0 -0
  112. corekit/serialization/enum.py +21 -0
  113. corekit/serialization/serializable.py +42 -0
  114. corekit/serialization/serializer.py +179 -0
  115. corekit/utils/__init__.py +5 -0
  116. corekit/utils/ids.py +5 -0
  117. corekit/utils/raise_exc.py +8 -0
  118. corekit/utils/time.py +21 -0
  119. corekit/utils/validators.py +15 -0
  120. corekit/utils/void.py +8 -0
  121. python_corekit-0.1.0.dist-info/METADATA +417 -0
  122. python_corekit-0.1.0.dist-info/RECORD +125 -0
  123. python_corekit-0.1.0.dist-info/WHEEL +5 -0
  124. python_corekit-0.1.0.dist-info/licenses/LICENSE +21 -0
  125. python_corekit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,37 @@
1
+ from typing import Any
2
+
3
+ from fastapi import status
4
+
5
+ from corekit.exceptions.base import CustomHTTPException
6
+
7
+
8
+ # 400 Errors
9
+ class UnauthorizedException(CustomHTTPException):
10
+ def __init__(self, **kwargs: Any) -> None:
11
+ super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, **kwargs)
12
+
13
+
14
+ class ForbiddenException(CustomHTTPException):
15
+ def __init__(self, **kwargs: Any) -> None:
16
+ super().__init__(status_code=status.HTTP_403_FORBIDDEN, **kwargs)
17
+
18
+
19
+ class NotFoundException(CustomHTTPException):
20
+ def __init__(self, **kwargs: Any) -> None:
21
+ super().__init__(status_code=status.HTTP_404_NOT_FOUND, **kwargs)
22
+
23
+
24
+ class ConflictErrorException(CustomHTTPException):
25
+ def __init__(self, **kwargs: Any) -> None:
26
+ super().__init__(status_code=status.HTTP_409_CONFLICT, **kwargs)
27
+
28
+
29
+ class UnprocessableEntityException(CustomHTTPException):
30
+ def __init__(self, **kwargs: Any) -> None:
31
+ super().__init__(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, **kwargs)
32
+
33
+
34
+ # 500 Errors
35
+ class InternalServerErrorException(CustomHTTPException):
36
+ def __init__(self, **kwargs: Any) -> None:
37
+ super().__init__(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, **kwargs)
@@ -0,0 +1,17 @@
1
+ """
2
+ Type aliases for exception classes.
3
+
4
+ These name the *class*, for signatures that accept an exception type rather than
5
+ an instance -- ``def handle(exc: CustomExceptionType) -> None``.
6
+
7
+ ``type(X)`` returns X's metaclass, which is ``type`` for an ordinary class and
8
+ carries no information about X. ``type[X]`` is the subscript form these need.
9
+ """
10
+
11
+ from corekit.exceptions.base import CustomException, CustomHTTPException
12
+
13
+ __all__ = ["ArbitraryCustomExceptionType", "CustomExceptionType", "CustomHTTPExceptionType"]
14
+
15
+ CustomExceptionType = type[CustomException]
16
+ CustomHTTPExceptionType = type[CustomHTTPException]
17
+ ArbitraryCustomExceptionType = CustomExceptionType | CustomHTTPExceptionType
@@ -0,0 +1,25 @@
1
+ """
2
+ File reading and writing, with format handled by the manager.
3
+
4
+ with JsonFileManager("data.json") as file:
5
+ data = file.read()
6
+
7
+ ``FileManager`` handles opening, closing and mode checking; a subclass supplies
8
+ ``_serialize`` and ``_deserialize`` for its format.
9
+ """
10
+
11
+ from corekit.files.base import FileContent, FileManager
12
+ from corekit.files.enum import FileError, FileMode
13
+ from corekit.files.json import JsonFileManager
14
+ from corekit.files.pickle import PickleFileManager
15
+ from corekit.files.toml import TomlFileManager
16
+
17
+ __all__ = [
18
+ "FileContent",
19
+ "FileError",
20
+ "FileManager",
21
+ "FileMode",
22
+ "JsonFileManager",
23
+ "PickleFileManager",
24
+ "TomlFileManager",
25
+ ]
corekit/files/base.py ADDED
@@ -0,0 +1,117 @@
1
+ from typing import Any, BinaryIO, Iterator, TextIO, Union
2
+
3
+ from corekit.files.enum import FileError, FileMode
4
+
5
+ FileContent: type[str | bytes] = Union[str, bytes]
6
+
7
+
8
+ class FileManager:
9
+ def __init__(
10
+ self,
11
+ file_path: str,
12
+ mode: FileMode | None = None,
13
+ binary: bool = False,
14
+ ) -> None:
15
+ self.file_path = file_path
16
+ self._mode = mode or FileMode.get_default(binary=binary)
17
+ self._fp: BinaryIO | TextIO | None = None
18
+
19
+ @property
20
+ def mode(self) -> FileMode:
21
+ return self._mode
22
+
23
+ @property
24
+ def is_open(self) -> bool:
25
+ """
26
+ Helper method to determine if the file is open
27
+ """
28
+ return self._fp is not None
29
+
30
+ @property
31
+ def can_write(self) -> bool:
32
+ """
33
+ Helper method to determine if the file can be written to
34
+ """
35
+ return self.is_open and self._mode.can_write()
36
+
37
+ def _verify_file_status(self, should_be_open: bool) -> None:
38
+ if self.is_open != should_be_open:
39
+ raise ValueError(FileError.NOT_OPEN if should_be_open else FileError.OPEN) # FIXME: raise custom exception
40
+
41
+ # ===== File Opening/Closing Methods =====
42
+ def _open(self) -> None:
43
+ """
44
+ Opens the file
45
+ """
46
+ self._verify_file_status(should_be_open=False)
47
+ self._fp = open(self.file_path, self.mode.value)
48
+
49
+ def _close(self) -> None:
50
+ self._verify_file_status(should_be_open=True)
51
+ self._fp.close()
52
+
53
+ # ===== Formatting Methods =====
54
+ def _serialize(self, content: Any) -> FileContent:
55
+ """
56
+ Helper method to serialize the content into the required format.
57
+ By default, does nothing and returns the input as is.
58
+ """
59
+ return content
60
+
61
+ def _deserialize(self, content: FileContent) -> Any:
62
+ """
63
+ Helper method to deserialize the content from the required format
64
+ into the working format. By default, does nothing and returns the
65
+ input as is.
66
+ """
67
+ return content
68
+
69
+ # ===== Context Manager Methods =====
70
+ def __enter__(self) -> "FileManager":
71
+ self._open()
72
+ return self
73
+
74
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
75
+ self._close()
76
+
77
+ # ===== File Reading Methods =====
78
+ def read(self, size: int = -1) -> str | bytes:
79
+ """
80
+ Reads content from the file
81
+ """
82
+ self._verify_file_status(should_be_open=True)
83
+ return self._deserialize(self._fp.read(size))
84
+
85
+ def readline(self) -> str | bytes:
86
+ """
87
+ Reads a single line from the file
88
+ """
89
+ self._verify_file_status(should_be_open=True)
90
+ return self._deserialize(self._fp.readline())
91
+
92
+ def stream(self) -> Iterator[str | bytes]:
93
+ """
94
+ Streams the file line by line
95
+ """
96
+ self._verify_file_status(should_be_open=True)
97
+ for line in self._fp:
98
+ yield self._deserialize(line)
99
+
100
+ # ===== File Writing Methods =====
101
+ def write(self, content: FileContent, newline: bool = False, skip_verification: bool = False) -> None:
102
+ """
103
+ Writes content to the file
104
+ """
105
+ if not skip_verification:
106
+ self._verify_file_status(should_be_open=True)
107
+ if newline:
108
+ content += "\n"
109
+ self._fp.write(self._serialize(content))
110
+
111
+ def writelines(self, lines: list[FileContent], newline: bool = True) -> None:
112
+ """
113
+ Writes multiple lines to the file
114
+ """
115
+ self._verify_file_status(should_be_open=True)
116
+ for line in lines:
117
+ self.write(line, newline=newline, skip_verification=True)
corekit/files/enum.py ADDED
@@ -0,0 +1,30 @@
1
+ from corekit.schemas.enum import StringEnum
2
+
3
+
4
+ class FileMode(StringEnum):
5
+ APPEND = "a"
6
+ APPEND_BINARY = "ab"
7
+ APPEND_WRITE = "a+"
8
+ APPEND_WRITE_BINARY = "a+b"
9
+ READ = "r"
10
+ READ_BINARY = "rb"
11
+ READ_WRITE = "r+"
12
+ READ_WRITE_BINARY = "r+b"
13
+ WRITE = "w"
14
+ WRITE_BINARY = "wb"
15
+
16
+ @classmethod
17
+ def get_default(cls, binary: bool = False) -> "FileMode":
18
+ if binary:
19
+ return cls.READ_BINARY
20
+ return cls.READ
21
+
22
+ def can_write(self) -> bool:
23
+ if self in {FileMode.READ, FileMode.READ_BINARY}:
24
+ return False
25
+ return True
26
+
27
+
28
+ class FileError(StringEnum):
29
+ NOT_OPEN = "File is not open"
30
+ OPEN = "File is open"
corekit/files/json.py ADDED
@@ -0,0 +1,12 @@
1
+ import json as _json
2
+ from typing import Any
3
+
4
+ from corekit.files.base import FileContent, FileManager
5
+
6
+
7
+ class JsonFileManager(FileManager):
8
+ def _serialize(self, content: Any) -> FileContent:
9
+ return _json.dumps(content)
10
+
11
+ def _deserialize(self, content: FileContent) -> Any:
12
+ return _json.loads(content)
@@ -0,0 +1,12 @@
1
+ import pickle
2
+ from typing import Any
3
+
4
+ from corekit.files.base import FileContent, FileManager
5
+
6
+
7
+ class PickleFileManager(FileManager):
8
+ def _serialize(self, content: Any) -> FileContent:
9
+ return pickle.dumps(content)
10
+
11
+ def _deserialize(self, content: FileContent) -> Any:
12
+ return pickle.loads(content)
corekit/files/toml.py ADDED
@@ -0,0 +1,43 @@
1
+ """
2
+ Reading TOML files.
3
+
4
+ with TomlFileManager("corekit.toml") as file:
5
+ settings = file.read()
6
+
7
+ Read-only: ``tomllib`` is the standard library's parser and it does not write.
8
+ Attempting to write raises rather than silently doing nothing.
9
+ """
10
+
11
+ import tomllib
12
+ from typing import Any
13
+
14
+ from corekit.files.base import FileContent, FileManager
15
+
16
+ __all__ = ["TomlFileManager"]
17
+
18
+
19
+ class TomlFileManager(FileManager):
20
+ """
21
+ A FileManager that parses TOML on read.
22
+
23
+ TOML must be read as bytes: ``tomllib.load`` decodes the file itself, so
24
+ that it can honour the encoding the format mandates.
25
+ """
26
+
27
+ def __init__(self, file_path: str, **kwargs: Any) -> None:
28
+ kwargs.setdefault("binary", True)
29
+ super().__init__(file_path, **kwargs)
30
+
31
+ def _deserialize(self, content: FileContent) -> Any:
32
+ """
33
+ Parse TOML text or bytes into a mapping.
34
+ """
35
+ if isinstance(content, str):
36
+ content = content.encode("utf-8")
37
+ return tomllib.loads(content.decode("utf-8"))
38
+
39
+ def _serialize(self, content: Any) -> FileContent:
40
+ """
41
+ Not supported: the standard library has no TOML writer.
42
+ """
43
+ raise NotImplementedError("TomlFileManager is read-only; tomllib does not write TOML.")
File without changes
corekit/http/client.py ADDED
@@ -0,0 +1,176 @@
1
+ """
2
+ A small HTTP client with retries.
3
+
4
+ Subclass and give it a base URL::
5
+
6
+ class GithubClient(BaseApiClient):
7
+ '''
8
+ Talks to the GitHub API.
9
+ '''
10
+
11
+ @property
12
+ def base_url(self) -> str:
13
+ return "https://api.github.com"
14
+
15
+ @property
16
+ def headers(self) -> dict[str, str]:
17
+ return {"Authorization": f"Bearer {self.token}"}
18
+
19
+ client = GithubClient()
20
+ response = client.get("/user") # or await client.async_get("/user")
21
+ response.data["login"]
22
+
23
+ Every response comes back as a ``BaseApiResponse``, so callers see one shape
24
+ regardless of what the endpoint returned. Requests that fail with a retryable
25
+ status are retried with exponential backoff.
26
+ """
27
+
28
+ from http import HTTPMethod
29
+ from typing import Any
30
+ from urllib.parse import urljoin
31
+
32
+ import httpx
33
+
34
+ from corekit.http.exponential_backoff import ExponentialBackoff
35
+ from corekit.http.response import BaseApiResponse
36
+ from corekit.observability.benchmarkable import Benchmarkable
37
+
38
+ __all__ = ["BaseApiClient", "URLMismatchError"]
39
+
40
+ # Statuses worth retrying: the server is busy or briefly unavailable, so the
41
+ # same request may well succeed shortly. A 4xx other than 429 will not.
42
+ RETRYABLE_STATUS_CODES = frozenset({429, 500, 502, 503, 504})
43
+
44
+
45
+ class URLMismatchError(Exception):
46
+ """
47
+ Raised when an absolute URL is passed that does not share the base URL.
48
+ """
49
+
50
+
51
+ class BaseApiClient(Benchmarkable):
52
+ """
53
+ Base for API clients. Override ``base_url`` and, usually, ``headers``.
54
+ """
55
+
56
+ def __init__(self, retries: int = 3, timeout: float = 30.0) -> None:
57
+ super().__init__()
58
+ self.retries = retries
59
+ self.timeout = timeout
60
+
61
+ @property
62
+ def headers(self) -> dict[str, str]:
63
+ """
64
+ Headers sent with every request.
65
+ """
66
+ return {}
67
+
68
+ @property
69
+ def base_url(self) -> str:
70
+ """
71
+ The root every relative path is resolved against.
72
+ """
73
+ return ""
74
+
75
+ def _build_url(self, url: str) -> str:
76
+ """
77
+ Resolve a path against the base URL, accepting an absolute URL that
78
+ already matches it.
79
+ """
80
+ if url.startswith(self.base_url):
81
+ return url
82
+ if url.startswith("http"):
83
+ raise URLMismatchError(f"URL {url} does not match base URL {self.base_url}")
84
+ return urljoin(self.base_url, url)
85
+
86
+ @staticmethod
87
+ def _to_response(raw: Any) -> BaseApiResponse:
88
+ """
89
+ Normalize an httpx response into a BaseApiResponse.
90
+
91
+ The body is decoded as JSON when it parses, and left as text otherwise,
92
+ so a caller never has to guard against a non-JSON error page.
93
+ """
94
+ try:
95
+ data = raw.json()
96
+ except ValueError:
97
+ data = {}
98
+ if not isinstance(data, dict):
99
+ data = {"data": data}
100
+
101
+ return BaseApiResponse(
102
+ status_code=raw.status_code,
103
+ text=raw.text,
104
+ data=data,
105
+ headers=dict(raw.headers),
106
+ cookies=dict(raw.cookies),
107
+ )
108
+
109
+ def _new_backoff(self) -> ExponentialBackoff:
110
+ return ExponentialBackoff(retries=self.retries)
111
+
112
+ def request(self, method: HTTPMethod, url: str, **kwargs: Any) -> BaseApiResponse:
113
+ """
114
+ Send a request, retrying retryable statuses.
115
+ """
116
+
117
+ target = self._build_url(url)
118
+ backoff = self._new_backoff()
119
+
120
+ while True:
121
+ with httpx.Client(timeout=self.timeout) as client:
122
+ raw = client.request(str(method), target, headers=self.headers, **kwargs)
123
+
124
+ if raw.status_code not in RETRYABLE_STATUS_CODES or backoff.did_timeout():
125
+ return self._to_response(raw)
126
+
127
+ self.warning(f"{method} {target} returned {raw.status_code}; retrying")
128
+ backoff.wait()
129
+
130
+ async def async_request(self, method: HTTPMethod, url: str, **kwargs: Any) -> BaseApiResponse:
131
+ """
132
+ Send a request asynchronously, retrying retryable statuses.
133
+ """
134
+
135
+ target = self._build_url(url)
136
+ backoff = self._new_backoff()
137
+
138
+ while True:
139
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
140
+ raw = await client.request(str(method), target, headers=self.headers, **kwargs)
141
+
142
+ if raw.status_code not in RETRYABLE_STATUS_CODES or backoff.did_timeout():
143
+ return self._to_response(raw)
144
+
145
+ self.warning(f"{method} {target} returned {raw.status_code}; retrying")
146
+ await backoff.async_wait()
147
+
148
+ def get(self, url: str, **kwargs: Any) -> BaseApiResponse:
149
+ return self.request(HTTPMethod.GET, url, **kwargs)
150
+
151
+ def post(self, url: str, **kwargs: Any) -> BaseApiResponse:
152
+ return self.request(HTTPMethod.POST, url, **kwargs)
153
+
154
+ def put(self, url: str, **kwargs: Any) -> BaseApiResponse:
155
+ return self.request(HTTPMethod.PUT, url, **kwargs)
156
+
157
+ def patch(self, url: str, **kwargs: Any) -> BaseApiResponse:
158
+ return self.request(HTTPMethod.PATCH, url, **kwargs)
159
+
160
+ def delete(self, url: str, **kwargs: Any) -> BaseApiResponse:
161
+ return self.request(HTTPMethod.DELETE, url, **kwargs)
162
+
163
+ async def async_get(self, url: str, **kwargs: Any) -> BaseApiResponse:
164
+ return await self.async_request(HTTPMethod.GET, url, **kwargs)
165
+
166
+ async def async_post(self, url: str, **kwargs: Any) -> BaseApiResponse:
167
+ return await self.async_request(HTTPMethod.POST, url, **kwargs)
168
+
169
+ async def async_put(self, url: str, **kwargs: Any) -> BaseApiResponse:
170
+ return await self.async_request(HTTPMethod.PUT, url, **kwargs)
171
+
172
+ async def async_patch(self, url: str, **kwargs: Any) -> BaseApiResponse:
173
+ return await self.async_request(HTTPMethod.PATCH, url, **kwargs)
174
+
175
+ async def async_delete(self, url: str, **kwargs: Any) -> BaseApiResponse:
176
+ return await self.async_request(HTTPMethod.DELETE, url, **kwargs)
@@ -0,0 +1,100 @@
1
+ """
2
+ Exponential backoff with jitter.
3
+
4
+ backoff = ExponentialBackoff(retries=5)
5
+ while True:
6
+ try:
7
+ return do_something()
8
+ except TransientError:
9
+ backoff.wait() # or `await backoff.async_wait()`
10
+
11
+ Delays double each attempt up to ``max_delay``, with jitter so that several
12
+ clients retrying together do not synchronize.
13
+ """
14
+
15
+ import asyncio
16
+ import random
17
+ import time
18
+
19
+ from corekit.exceptions.base import ExponentialBackoffTimeoutException
20
+ from corekit.observability.loggable import Loggable
21
+
22
+ __all__ = ["ExponentialBackoff"]
23
+
24
+
25
+ class ExponentialBackoff(Loggable):
26
+ """
27
+ Tracks retry attempts and sleeps for an increasing delay between them.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ retries: int = 5,
33
+ base_delay: float = 1,
34
+ max_delay: float = 32,
35
+ jitter: bool = True,
36
+ exc_to_raise: BaseException | None = None,
37
+ ) -> None:
38
+ """
39
+ :param retries: attempts allowed before giving up.
40
+ :param base_delay: delay before the first retry, in seconds.
41
+ :param max_delay: ceiling for any single delay, in seconds.
42
+ :param jitter: randomize each delay, so concurrent clients desynchronize.
43
+ :param exc_to_raise: raised on exhaustion instead of the default. Use it
44
+ to surface the original failure rather than masking it with a
45
+ timeout that says nothing about what actually went wrong.
46
+ """
47
+ super().__init__()
48
+ self.retries = retries
49
+ self.base_delay = base_delay
50
+ self.max_delay = max_delay
51
+ self.jitter = jitter
52
+ self.exc_to_raise = exc_to_raise
53
+ self.attempt_num = 0
54
+
55
+ def did_timeout(self) -> bool:
56
+ """
57
+ Whether the allowed attempts are used up.
58
+ """
59
+ return self.attempt_num >= self.retries
60
+
61
+ def _get_delay(self) -> float:
62
+ """
63
+ The delay for the current attempt, raising once attempts are exhausted.
64
+ """
65
+ if self.did_timeout():
66
+ if self.exc_to_raise is not None:
67
+ raise self.exc_to_raise
68
+ raise ExponentialBackoffTimeoutException(attempts=self.attempt_num)
69
+
70
+ delay = min(self.base_delay * (2**self.attempt_num), self.max_delay)
71
+ if self.jitter:
72
+ delay *= random.uniform(0.5, 1.5)
73
+ return delay
74
+
75
+ def wait(self) -> None:
76
+ """
77
+ Block for the current delay, then advance the attempt counter.
78
+ """
79
+ delay = self._get_delay()
80
+ self.info(f"Attempt {self.attempt_num}, waiting {delay:.2f} seconds...")
81
+ time.sleep(delay)
82
+ self.attempt_num += 1
83
+
84
+ async def async_wait(self) -> None:
85
+ """
86
+ Await the current delay, then advance the attempt counter.
87
+
88
+ Use this in async code: ``wait`` blocks the event loop, stalling every
89
+ other task in the process.
90
+ """
91
+ delay = self._get_delay()
92
+ self.info(f"Attempt {self.attempt_num}, waiting {delay:.2f} seconds...")
93
+ await asyncio.sleep(delay)
94
+ self.attempt_num += 1
95
+
96
+ def restart(self) -> None:
97
+ """
98
+ Reset the attempt counter.
99
+ """
100
+ self.attempt_num = 0
@@ -0,0 +1,12 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from corekit.schemas.pydantic.fields import DefaultDictField
4
+ from corekit.schemas.types import AnyDict
5
+
6
+
7
+ class BaseApiResponse(BaseModel):
8
+ status_code: int = Field(..., description="HTTP status code")
9
+ text: str = Field(..., description="Response text")
10
+ data: AnyDict = DefaultDictField()
11
+ headers: AnyDict = DefaultDictField()
12
+ cookies: AnyDict = DefaultDictField()
@@ -0,0 +1,23 @@
1
+ """
2
+ Log-driven container automation.
3
+ """
4
+
5
+ from corekit.log_monitor.models import (
6
+ Action,
7
+ ActionType,
8
+ ContainerConfig,
9
+ LogMonitorConfig,
10
+ Rule,
11
+ Severity,
12
+ )
13
+ from corekit.log_monitor.service import LogMonitor
14
+
15
+ __all__ = [
16
+ "Action",
17
+ "ActionType",
18
+ "ContainerConfig",
19
+ "LogMonitor",
20
+ "LogMonitorConfig",
21
+ "Rule",
22
+ "Severity",
23
+ ]
@@ -0,0 +1,8 @@
1
+ DEFAULT_CONFIG_PATH = "config.yaml"
2
+
3
+ # Bounded histories used for rate limiting, so memory does not grow with uptime.
4
+ ACTION_COUNTS_MAX_LEN = 100
5
+ RESTART_COUNTS_MAX_LEN = 10
6
+
7
+ DEFAULT_MAX_WORKERS = 20
8
+ SHUTDOWN_TIMEOUT_SECONDS = 30