recce-nightly 0.62.0.20250417__py3-none-any.whl → 1.30.0.20251221__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.

Potentially problematic release.


This version of recce-nightly might be problematic. Click here for more details.

Files changed (245) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +27 -22
  3. recce/adapter/base.py +11 -14
  4. recce/adapter/dbt_adapter/__init__.py +845 -461
  5. recce/adapter/dbt_adapter/dbt_version.py +3 -0
  6. recce/adapter/sqlmesh_adapter.py +24 -35
  7. recce/apis/check_api.py +59 -42
  8. recce/apis/check_events_api.py +353 -0
  9. recce/apis/check_func.py +41 -35
  10. recce/apis/run_api.py +25 -19
  11. recce/apis/run_func.py +64 -25
  12. recce/artifact.py +119 -51
  13. recce/cli.py +1301 -324
  14. recce/config.py +43 -34
  15. recce/connect_to_cloud.py +138 -0
  16. recce/core.py +55 -47
  17. recce/data/404/index.html +2 -0
  18. recce/data/404.html +2 -1
  19. recce/data/__next.@lineage.!KHNsb3Qp.__PAGE__.txt +7 -0
  20. recce/data/__next.@lineage.!KHNsb3Qp.txt +4 -0
  21. recce/data/__next.__PAGE__.txt +6 -0
  22. recce/data/__next._full.txt +32 -0
  23. recce/data/__next._head.txt +8 -0
  24. recce/data/__next._index.txt +14 -0
  25. recce/data/__next._tree.txt +8 -0
  26. recce/data/_next/static/chunks/025a7e3e3f9f40ae.js +1 -0
  27. recce/data/_next/static/chunks/0ce56d67ef5779ca.js +4 -0
  28. recce/data/_next/static/chunks/1a6a78780155dac7.js +48 -0
  29. recce/data/_next/static/chunks/1de8485918b9182a.css +2 -0
  30. recce/data/_next/static/chunks/1e4b1b50d1e34993.js +1 -0
  31. recce/data/_next/static/chunks/206d5d181e4c738e.js +1 -0
  32. recce/data/_next/static/chunks/2c357efc34c5b859.js +25 -0
  33. recce/data/_next/static/chunks/2e9d95d2d48c479c.js +1 -0
  34. recce/data/_next/static/chunks/2f016dc4a3edad2e.js +2 -0
  35. recce/data/_next/static/chunks/313251962d698f7c.js +1 -0
  36. recce/data/_next/static/chunks/3a9f021f38eb5574.css +1 -0
  37. recce/data/_next/static/chunks/40079da8d2b8f651.js +1 -0
  38. recce/data/_next/static/chunks/4599182bffb64661.js +38 -0
  39. recce/data/_next/static/chunks/4e62f6e184173580.js +1 -0
  40. recce/data/_next/static/chunks/5c4dfb0d09eaa401.js +1 -0
  41. recce/data/_next/static/chunks/69e4f06ccfdfc3ac.js +1 -0
  42. recce/data/_next/static/chunks/6b206cb4707d6bee.js +1 -0
  43. recce/data/_next/static/chunks/6d8557f062aa4386.css +1 -0
  44. recce/data/_next/static/chunks/7fbe3650bd83b6b5.js +1 -0
  45. recce/data/_next/static/chunks/83fa823a825674f6.js +1 -0
  46. recce/data/_next/static/chunks/848a6c9b5f55f7ed.js +1 -0
  47. recce/data/_next/static/chunks/859462b0858aef88.css +2 -0
  48. recce/data/_next/static/chunks/923964f18c87d0f1.css +1 -0
  49. recce/data/_next/static/chunks/939390f911895d7c.js +48 -0
  50. recce/data/_next/static/chunks/99a9817237a07f43.js +1 -0
  51. recce/data/_next/static/chunks/9fed8b4b2b924054.js +5 -0
  52. recce/data/_next/static/chunks/b6949f6c5892110c.js +1 -0
  53. recce/data/_next/static/chunks/b851a1d3f8149828.js +1 -0
  54. recce/data/_next/static/chunks/c734f9ad957de0b4.js +1 -0
  55. recce/data/_next/static/chunks/cdde321b0ec75717.js +2 -0
  56. recce/data/_next/static/chunks/d0f91117d77ff844.css +1 -0
  57. recce/data/_next/static/chunks/d6c8667911c2500f.js +1 -0
  58. recce/data/_next/static/chunks/da8dab68c02752cf.js +74 -0
  59. recce/data/_next/static/chunks/dc074049c9d12d97.js +109 -0
  60. recce/data/_next/static/chunks/ee7f1a8227342421.js +1 -0
  61. recce/data/_next/static/chunks/fa2f4e56c2fccc73.js +1 -0
  62. recce/data/_next/static/chunks/turbopack-1fad664f62979b93.js +3 -0
  63. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  64. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  65. recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
  66. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  67. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
  68. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  69. recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
  70. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  71. recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
  72. recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
  73. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  74. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_buildManifest.js +11 -0
  75. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_clientMiddlewareManifest.json +1 -0
  76. recce/data/_not-found/__next._full.txt +24 -0
  77. recce/data/_not-found/__next._head.txt +8 -0
  78. recce/data/_not-found/__next._index.txt +13 -0
  79. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  80. recce/data/_not-found/__next._not-found.txt +4 -0
  81. recce/data/_not-found/__next._tree.txt +6 -0
  82. recce/data/_not-found/index.html +2 -0
  83. recce/data/_not-found/index.txt +24 -0
  84. recce/data/auth_callback.html +68 -0
  85. recce/data/checks/__next.@lineage.__DEFAULT__.txt +7 -0
  86. recce/data/checks/__next._full.txt +39 -0
  87. recce/data/checks/__next._head.txt +8 -0
  88. recce/data/checks/__next._index.txt +14 -0
  89. recce/data/checks/__next._tree.txt +8 -0
  90. recce/data/checks/__next.checks.__PAGE__.txt +10 -0
  91. recce/data/checks/__next.checks.txt +4 -0
  92. recce/data/checks/index.html +2 -0
  93. recce/data/checks/index.txt +39 -0
  94. recce/data/imgs/reload-image.svg +4 -0
  95. recce/data/index.html +2 -27
  96. recce/data/index.txt +32 -7
  97. recce/data/lineage/__next.@lineage.__DEFAULT__.txt +7 -0
  98. recce/data/lineage/__next._full.txt +39 -0
  99. recce/data/lineage/__next._head.txt +8 -0
  100. recce/data/lineage/__next._index.txt +14 -0
  101. recce/data/lineage/__next._tree.txt +8 -0
  102. recce/data/lineage/__next.lineage.__PAGE__.txt +10 -0
  103. recce/data/lineage/__next.lineage.txt +4 -0
  104. recce/data/lineage/index.html +2 -0
  105. recce/data/lineage/index.txt +39 -0
  106. recce/data/query/__next.@lineage.__DEFAULT__.txt +7 -0
  107. recce/data/query/__next._full.txt +37 -0
  108. recce/data/query/__next._head.txt +8 -0
  109. recce/data/query/__next._index.txt +14 -0
  110. recce/data/query/__next._tree.txt +8 -0
  111. recce/data/query/__next.query.__PAGE__.txt +9 -0
  112. recce/data/query/__next.query.txt +4 -0
  113. recce/data/query/index.html +2 -0
  114. recce/data/query/index.txt +37 -0
  115. recce/diff.py +6 -12
  116. recce/event/CONFIG.bak +1 -0
  117. recce/event/__init__.py +86 -74
  118. recce/event/collector.py +33 -22
  119. recce/event/track.py +49 -27
  120. recce/exceptions.py +1 -1
  121. recce/git.py +7 -7
  122. recce/github.py +57 -53
  123. recce/mcp_server.py +725 -0
  124. recce/models/__init__.py +4 -1
  125. recce/models/check.py +438 -21
  126. recce/models/run.py +1 -0
  127. recce/models/types.py +134 -28
  128. recce/pull_request.py +27 -25
  129. recce/run.py +179 -122
  130. recce/server.py +394 -104
  131. recce/state/__init__.py +31 -0
  132. recce/state/cloud.py +644 -0
  133. recce/state/const.py +26 -0
  134. recce/state/local.py +56 -0
  135. recce/state/state.py +119 -0
  136. recce/state/state_loader.py +174 -0
  137. recce/summary.py +196 -149
  138. recce/tasks/__init__.py +19 -3
  139. recce/tasks/core.py +11 -13
  140. recce/tasks/dataframe.py +82 -18
  141. recce/tasks/histogram.py +69 -34
  142. recce/tasks/lineage.py +2 -2
  143. recce/tasks/profile.py +152 -86
  144. recce/tasks/query.py +180 -89
  145. recce/tasks/rowcount.py +37 -31
  146. recce/tasks/schema.py +18 -15
  147. recce/tasks/top_k.py +35 -35
  148. recce/tasks/utils.py +147 -0
  149. recce/tasks/valuediff.py +247 -155
  150. recce/util/__init__.py +3 -0
  151. recce/util/api_token.py +80 -0
  152. recce/util/breaking.py +105 -100
  153. recce/util/cll.py +274 -219
  154. recce/util/cloud/__init__.py +15 -0
  155. recce/util/cloud/base.py +115 -0
  156. recce/util/cloud/check_events.py +190 -0
  157. recce/util/cloud/checks.py +242 -0
  158. recce/util/io.py +22 -17
  159. recce/util/lineage.py +65 -16
  160. recce/util/logger.py +1 -1
  161. recce/util/onboarding_state.py +45 -0
  162. recce/util/perf_tracking.py +85 -0
  163. recce/util/recce_cloud.py +347 -72
  164. recce/util/singleton.py +4 -4
  165. recce/util/startup_perf.py +121 -0
  166. recce/yaml/__init__.py +7 -10
  167. recce_nightly-1.30.0.20251221.dist-info/METADATA +195 -0
  168. recce_nightly-1.30.0.20251221.dist-info/RECORD +183 -0
  169. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/WHEEL +1 -2
  170. recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
  171. recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
  172. recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
  173. recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
  174. recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
  175. recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
  176. recce/data/_next/static/chunks/500-e51c92a025a51234.js +0 -65
  177. recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
  178. recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
  179. recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
  180. recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
  181. recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
  182. recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
  183. recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
  184. recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
  185. recce/data/_next/static/chunks/app/page-9adc25782272ed2e.js +0 -1
  186. recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
  187. recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
  188. recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
  189. recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
  190. recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
  191. recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
  192. recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
  193. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  194. recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
  195. recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
  196. recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
  197. recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
  198. recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
  199. recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
  200. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  201. recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
  202. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
  203. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  204. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  205. recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
  206. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  207. recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
  208. recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
  209. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  210. recce/data/_next/static/qiyFlux77VkhxiceAJe_F/_buildManifest.js +0 -1
  211. recce/state.py +0 -753
  212. recce_nightly-0.62.0.20250417.dist-info/METADATA +0 -311
  213. recce_nightly-0.62.0.20250417.dist-info/RECORD +0 -139
  214. recce_nightly-0.62.0.20250417.dist-info/top_level.txt +0 -2
  215. tests/__init__.py +0 -0
  216. tests/adapter/__init__.py +0 -0
  217. tests/adapter/dbt_adapter/__init__.py +0 -0
  218. tests/adapter/dbt_adapter/conftest.py +0 -13
  219. tests/adapter/dbt_adapter/dbt_test_helper.py +0 -283
  220. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -40
  221. tests/adapter/dbt_adapter/test_dbt_cll.py +0 -102
  222. tests/adapter/dbt_adapter/test_selector.py +0 -177
  223. tests/tasks/__init__.py +0 -0
  224. tests/tasks/conftest.py +0 -4
  225. tests/tasks/test_histogram.py +0 -137
  226. tests/tasks/test_lineage.py +0 -42
  227. tests/tasks/test_preset_checks.py +0 -50
  228. tests/tasks/test_profile.py +0 -73
  229. tests/tasks/test_query.py +0 -151
  230. tests/tasks/test_row_count.py +0 -116
  231. tests/tasks/test_schema.py +0 -99
  232. tests/tasks/test_top_k.py +0 -73
  233. tests/tasks/test_valuediff.py +0 -74
  234. tests/test_cli.py +0 -122
  235. tests/test_config.py +0 -45
  236. tests/test_core.py +0 -27
  237. tests/test_dbt.py +0 -36
  238. tests/test_pull_request.py +0 -130
  239. tests/test_server.py +0 -98
  240. tests/test_state.py +0 -123
  241. tests/test_summary.py +0 -57
  242. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  243. /recce/data/_next/static/{qiyFlux77VkhxiceAJe_F → nX-Uz0AH6Tc6hIQUFGqaB}/_ssgManifest.js +0 -0
  244. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/entry_points.txt +0 -0
  245. {recce_nightly-0.62.0.20250417.dist-info → recce_nightly-1.30.0.20251221.dist-info}/licenses/LICENSE +0 -0
recce/state/const.py ADDED
@@ -0,0 +1,26 @@
1
+ from dataclasses import dataclass
2
+
3
+ RECCE_STATE_FILE = "recce-state.json"
4
+ RECCE_STATE_COMPRESSED_FILE = f"{RECCE_STATE_FILE}.gz"
5
+
6
+
7
+ @dataclass
8
+ class ErrorMessage:
9
+ error_message: str
10
+ hint_message: str
11
+
12
+
13
+ RECCE_CLOUD_TOKEN_MISSING = ErrorMessage(
14
+ error_message="No GitHub token is provided to access the pull request information",
15
+ hint_message="Please provide a GitHub token in the command argument",
16
+ )
17
+
18
+ RECCE_CLOUD_PASSWORD_MISSING = ErrorMessage(
19
+ error_message="No password provided to access the state file in Recce Cloud",
20
+ hint_message='Please provide a password with the option "--password <compress-password>"',
21
+ )
22
+
23
+ RECCE_API_TOKEN_MISSING = ErrorMessage(
24
+ error_message="No Recce API token is provided",
25
+ hint_message="Please login to Recce Cloud and copy the API token from the settings page",
26
+ )
recce/state/local.py ADDED
@@ -0,0 +1,56 @@
1
+ import logging
2
+ import os
3
+ from typing import Optional, Tuple, Union
4
+
5
+ from .state import RecceState
6
+ from .state_loader import RecceStateLoader
7
+
8
+ logger = logging.getLogger("uvicorn")
9
+
10
+
11
+ class FileStateLoader(RecceStateLoader):
12
+ def __init__(
13
+ self,
14
+ review_mode: bool = False,
15
+ state_file: Optional[str] = None,
16
+ initial_state: Optional[RecceState] = None,
17
+ ):
18
+ super().__init__(review_mode=review_mode, state_file=state_file, initial_state=initial_state)
19
+
20
+ def verify(self) -> bool:
21
+ if self.review_mode is True and self.state_file is None:
22
+ self.error_message = "Recce can not launch without a state file."
23
+ self.hint_message = "Please provide a state file in the command argument."
24
+ return False
25
+ return True
26
+
27
+ def _load_state(self) -> Tuple[RecceState, str]:
28
+ state = RecceState.from_file(self.state_file) if self.state_file else None
29
+ state_tag = None
30
+ return state, state_tag
31
+
32
+ def _export_state(self, state: RecceState = None) -> Tuple[Union[str, None], str]:
33
+ """
34
+ Store the state to a file. Store happens when terminating the server or run instance.
35
+ """
36
+
37
+ if self.state_file is None:
38
+ return "No state file is provided. Skip storing the state.", None
39
+
40
+ logger.info(f"Store recce state to '{self.state_file}'")
41
+ message = self._export_state_to_file(self.state_file)
42
+ tag = None
43
+
44
+ return message, tag
45
+
46
+ def purge(self) -> bool:
47
+ if self.state_file is not None:
48
+ try:
49
+ os.remove(self.state_file)
50
+ return True
51
+ except Exception as e:
52
+ self.error_message = f"Failed to remove the state file: {e}"
53
+ return False
54
+ else:
55
+ self.error_message = "No state file is provided. Skip removing the state file."
56
+ return False
recce/state/state.py ADDED
@@ -0,0 +1,119 @@
1
+ """Define the type to serialize/de-serialize the state of the recce instance."""
2
+
3
+ import json
4
+ import logging
5
+ from datetime import datetime
6
+ from typing import Dict, List, Optional
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from recce import get_version
11
+ from recce.exceptions import RecceException
12
+ from recce.git import current_branch
13
+ from recce.models.types import Check, Run
14
+ from recce.pull_request import PullRequestInfo
15
+ from recce.util.io import SupportedFileTypes, file_io_factory
16
+ from recce.util.pydantic_model import pydantic_model_dump, pydantic_model_json_dump
17
+
18
+ logger = logging.getLogger("uvicorn")
19
+
20
+
21
+ class GitRepoInfo(BaseModel):
22
+ branch: Optional[str] = None
23
+
24
+ @staticmethod
25
+ def from_current_repository() -> Optional["GitRepoInfo"]:
26
+ branch = current_branch()
27
+ if branch is None:
28
+ return None
29
+
30
+ return GitRepoInfo(branch=branch)
31
+
32
+ def to_dict(self):
33
+ return pydantic_model_dump(self)
34
+
35
+
36
+ class RecceStateMetadata(BaseModel):
37
+ schema_version: str = "v0"
38
+ recce_version: str = Field(default_factory=lambda: get_version())
39
+ generated_at: str = Field(default_factory=lambda: datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"))
40
+
41
+
42
+ class ArtifactsRoot(BaseModel):
43
+ """
44
+ Root of the artifacts.
45
+
46
+ base: artifacts of the base env. key is file name, value is dict
47
+ current: artifacts of the current env. key is file name, value is dict
48
+ """
49
+
50
+ base: Dict[str, Optional[dict]] = {}
51
+ current: Dict[str, Optional[dict]] = {}
52
+
53
+
54
+ class RecceState(BaseModel):
55
+ metadata: Optional[RecceStateMetadata] = None
56
+ runs: Optional[List[Run]] = Field(default_factory=list)
57
+ checks: Optional[List[Check]] = Field(default_factory=list)
58
+ artifacts: ArtifactsRoot = ArtifactsRoot(base={}, current={})
59
+ git: Optional[GitRepoInfo] = None
60
+ pull_request: Optional[PullRequestInfo] = None
61
+
62
+ @staticmethod
63
+ def from_json(json_content: str):
64
+ dict_data = json.loads(json_content)
65
+ state = RecceState(**dict_data)
66
+ metadata = state.metadata
67
+
68
+ if metadata:
69
+ if metadata.schema_version is None:
70
+ pass
71
+ if metadata.schema_version == "v0":
72
+ pass
73
+ else:
74
+ raise RecceException(f"Unsupported state file version: {metadata.schema_version}")
75
+ return state
76
+
77
+ @staticmethod
78
+ def from_file(file_path: str, file_type: SupportedFileTypes = SupportedFileTypes.FILE):
79
+ """
80
+ Load the state from a recce state file.
81
+ """
82
+ from pathlib import Path
83
+
84
+ logger.debug(f"Load state file from: '{file_path}'")
85
+ if not Path(file_path).is_file():
86
+ return None
87
+
88
+ io = file_io_factory(file_type)
89
+ json_content = io.read(file_path)
90
+ return RecceState.from_json(json_content)
91
+
92
+ def to_json(self):
93
+ return pydantic_model_json_dump(self)
94
+
95
+ def to_file(self, file_path: str, file_type: SupportedFileTypes = SupportedFileTypes.FILE):
96
+
97
+ json_data = self.to_json()
98
+ io = file_io_factory(file_type)
99
+
100
+ io.write(file_path, json_data)
101
+ return f"The state file is stored at '{file_path}'"
102
+
103
+ def _merge_run(self, run: Run):
104
+ for r in self.runs:
105
+ if r.run_id == run.run_id:
106
+ break
107
+ else:
108
+ self.runs.append(run)
109
+
110
+ def _merge_check(self, check: Check):
111
+ for c in self.checks:
112
+ if c.check_id == check.check_id:
113
+ c.merge(check)
114
+ break
115
+ else:
116
+ self.checks.append(check)
117
+
118
+ def _merge_artifacts(self, artifacts: ArtifactsRoot):
119
+ self.artifacts.merge(artifacts)
@@ -0,0 +1,174 @@
1
+ import logging
2
+ import threading
3
+ import time
4
+ from abc import ABC, abstractmethod
5
+ from typing import Dict, Literal, Optional, Tuple, Union, final
6
+
7
+ from recce.exceptions import RecceException
8
+ from recce.pull_request import fetch_pr_metadata
9
+
10
+ from ..util.io import SupportedFileTypes, file_io_factory
11
+ from .const import RECCE_API_TOKEN_MISSING
12
+ from .state import RecceState
13
+
14
+ logger = logging.getLogger("uvicorn")
15
+
16
+
17
+ class RecceStateLoader(ABC):
18
+ def __init__(
19
+ self,
20
+ review_mode: bool = False,
21
+ cloud_mode: bool = False,
22
+ state_file: Optional[str] = None,
23
+ cloud_options: Optional[Dict[str, str]] = None,
24
+ initial_state: Optional[RecceState] = None,
25
+ ):
26
+ self.review_mode = review_mode
27
+ self.cloud_mode = cloud_mode
28
+ self.state_file = state_file
29
+ self.cloud_options = cloud_options or {}
30
+ self.error_message = None
31
+ self.hint_message = None
32
+ self.state: RecceState | None = initial_state
33
+ self.state_lock = threading.Lock()
34
+ self.state_etag = None
35
+ self.pr_info = None
36
+ self.catalog: Literal["github", "preview", "session"] = "github"
37
+ self.share_id = None
38
+ self.session_id = None
39
+
40
+ if self.cloud_mode:
41
+ if self.cloud_options.get("github_token"):
42
+ self.catalog = "github"
43
+ self.pr_info = fetch_pr_metadata(
44
+ cloud=self.cloud_mode, github_token=self.cloud_options.get("github_token")
45
+ )
46
+ if self.pr_info.id is None:
47
+ raise RecceException("Cannot get the pull request information from GitHub.")
48
+ elif self.cloud_options.get("api_token"):
49
+ if self.cloud_options.get("session_id"):
50
+ self.catalog = "session"
51
+ self.session_id = self.cloud_options.get("session_id")
52
+ else:
53
+ self.catalog = "preview"
54
+ self.share_id = self.cloud_options.get("share_id")
55
+ else:
56
+ raise RecceException(RECCE_API_TOKEN_MISSING.error_message)
57
+
58
+ @property
59
+ def token(self):
60
+ return self.cloud_options.get("github_token") or self.cloud_options.get("api_token")
61
+
62
+ @abstractmethod
63
+ def verify(self) -> bool:
64
+ """
65
+ Verify the state loader configuration.
66
+ Returns:
67
+ bool: True if the configuration is valid, False otherwise.
68
+ """
69
+ raise NotImplementedError("Subclasses must implement this method.")
70
+
71
+ @property
72
+ def error_and_hint(self) -> (Union[str, None], Union[str, None]):
73
+ return self.error_message, self.hint_message
74
+
75
+ def update(self, state: RecceState):
76
+ self.state = state
77
+
78
+ @final
79
+ def load(self, refresh=False) -> RecceState:
80
+ if self.state is not None and refresh is False:
81
+ return self.state
82
+ self.state_lock.acquire()
83
+ try:
84
+ self.state, self.state_etag = self._load_state()
85
+ finally:
86
+ self.state_lock.release()
87
+ return self.state
88
+
89
+ @abstractmethod
90
+ def _load_state(self) -> Tuple[RecceState, str]:
91
+ """
92
+ Load the state from the specified source (file or cloud).
93
+ Returns:
94
+ RecceState: The loaded state object.
95
+ str: The etag of the state file (if applicable).
96
+ """
97
+ raise NotImplementedError("Subclasses must implement this method.")
98
+
99
+ def save_as(self, state_file: str, state: RecceState = None):
100
+ if self.cloud_mode:
101
+ raise Exception("Cannot save the state to Recce Cloud.")
102
+
103
+ self.state_file = state_file
104
+ self.export(state)
105
+
106
+ @final
107
+ def export(self, state: RecceState = None) -> Union[str, None]:
108
+ if state is not None:
109
+ self.update(state)
110
+
111
+ start_time = time.time()
112
+ self.state_lock.acquire()
113
+ try:
114
+ message, state_etag = self._export_state()
115
+ self.state_etag = state_etag
116
+ end_time = time.time()
117
+ elapsed_time = end_time - start_time
118
+ finally:
119
+ self.state_lock.release()
120
+ logger.info(f"Store state completed in {elapsed_time:.2f} seconds")
121
+ return message
122
+
123
+ @abstractmethod
124
+ def _export_state(self) -> Tuple[Union[str, None], str]:
125
+ """
126
+ Export the current Recce state to a file or cloud storage.
127
+ Returns:
128
+ str: A message indicating the result of the export operation.
129
+ str: The etag of the exported state file (if applicable).
130
+ """
131
+ raise NotImplementedError("Subclasses must implement this method.")
132
+
133
+ def _export_state_to_file(self, file_path: str, file_type: SupportedFileTypes = SupportedFileTypes.FILE) -> str:
134
+ """
135
+ Store the state to a file. Store happens when terminating the server or run instance.
136
+ """
137
+
138
+ json_data = self.state.to_json()
139
+ io = file_io_factory(file_type)
140
+
141
+ io.write(file_path, json_data)
142
+ return f"The state file is stored at '{file_path}'"
143
+
144
+ def refresh(self):
145
+ new_state = self.load(refresh=True)
146
+ return new_state
147
+
148
+ def check_conflict(self) -> bool:
149
+ return False
150
+
151
+ def info(self) -> dict:
152
+ if self.state is None:
153
+ self.error_message = "No state is loaded."
154
+ return None
155
+
156
+ state_info = {
157
+ "mode": "cloud" if self.cloud_mode else "local",
158
+ "source": None,
159
+ }
160
+ if self.cloud_mode:
161
+ state_info["source"] = "Recce Cloud"
162
+ state_info["pull_request"] = self.pr_info
163
+ else:
164
+ state_info["source"] = self.state_file
165
+ return state_info
166
+
167
+ @abstractmethod
168
+ def purge(self) -> bool:
169
+ """
170
+ Purge the state file or cloud storage.
171
+ Returns:
172
+ bool: True if the purge was successful, False otherwise.
173
+ """
174
+ raise NotImplementedError("Subclasses must implement this method.")