recce-nightly 1.10.0.20250625__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 (229) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +5 -0
  3. recce/adapter/dbt_adapter/__init__.py +343 -245
  4. recce/apis/check_api.py +20 -14
  5. recce/apis/check_events_api.py +353 -0
  6. recce/apis/check_func.py +5 -5
  7. recce/apis/run_func.py +32 -3
  8. recce/artifact.py +76 -3
  9. recce/cli.py +705 -82
  10. recce/config.py +2 -2
  11. recce/connect_to_cloud.py +1 -1
  12. recce/core.py +3 -3
  13. recce/data/404/index.html +2 -0
  14. recce/data/404.html +2 -22
  15. recce/data/__next.@lineage.!KHNsb3Qp.__PAGE__.txt +7 -0
  16. recce/data/__next.@lineage.!KHNsb3Qp.txt +4 -0
  17. recce/data/__next.__PAGE__.txt +6 -0
  18. recce/data/__next._full.txt +32 -0
  19. recce/data/__next._head.txt +8 -0
  20. recce/data/__next._index.txt +14 -0
  21. recce/data/__next._tree.txt +8 -0
  22. recce/data/_next/static/chunks/025a7e3e3f9f40ae.js +1 -0
  23. recce/data/_next/static/chunks/0ce56d67ef5779ca.js +4 -0
  24. recce/data/_next/static/chunks/1a6a78780155dac7.js +48 -0
  25. recce/data/_next/static/chunks/1de8485918b9182a.css +2 -0
  26. recce/data/_next/static/chunks/1e4b1b50d1e34993.js +1 -0
  27. recce/data/_next/static/chunks/206d5d181e4c738e.js +1 -0
  28. recce/data/_next/static/chunks/2c357efc34c5b859.js +25 -0
  29. recce/data/_next/static/chunks/2e9d95d2d48c479c.js +1 -0
  30. recce/data/_next/static/chunks/2f016dc4a3edad2e.js +2 -0
  31. recce/data/_next/static/chunks/313251962d698f7c.js +1 -0
  32. recce/data/_next/static/chunks/3a9f021f38eb5574.css +1 -0
  33. recce/data/_next/static/chunks/40079da8d2b8f651.js +1 -0
  34. recce/data/_next/static/chunks/4599182bffb64661.js +38 -0
  35. recce/data/_next/static/chunks/4e62f6e184173580.js +1 -0
  36. recce/data/_next/static/chunks/5c4dfb0d09eaa401.js +1 -0
  37. recce/data/_next/static/chunks/69e4f06ccfdfc3ac.js +1 -0
  38. recce/data/_next/static/chunks/6b206cb4707d6bee.js +1 -0
  39. recce/data/_next/static/chunks/6d8557f062aa4386.css +1 -0
  40. recce/data/_next/static/chunks/7fbe3650bd83b6b5.js +1 -0
  41. recce/data/_next/static/chunks/83fa823a825674f6.js +1 -0
  42. recce/data/_next/static/chunks/848a6c9b5f55f7ed.js +1 -0
  43. recce/data/_next/static/chunks/859462b0858aef88.css +2 -0
  44. recce/data/_next/static/chunks/923964f18c87d0f1.css +1 -0
  45. recce/data/_next/static/chunks/939390f911895d7c.js +48 -0
  46. recce/data/_next/static/chunks/99a9817237a07f43.js +1 -0
  47. recce/data/_next/static/chunks/9fed8b4b2b924054.js +5 -0
  48. recce/data/_next/static/chunks/b6949f6c5892110c.js +1 -0
  49. recce/data/_next/static/chunks/b851a1d3f8149828.js +1 -0
  50. recce/data/_next/static/chunks/c734f9ad957de0b4.js +1 -0
  51. recce/data/_next/static/chunks/cdde321b0ec75717.js +2 -0
  52. recce/data/_next/static/chunks/d0f91117d77ff844.css +1 -0
  53. recce/data/_next/static/chunks/d6c8667911c2500f.js +1 -0
  54. recce/data/_next/static/chunks/da8dab68c02752cf.js +74 -0
  55. recce/data/_next/static/chunks/dc074049c9d12d97.js +109 -0
  56. recce/data/_next/static/chunks/ee7f1a8227342421.js +1 -0
  57. recce/data/_next/static/chunks/fa2f4e56c2fccc73.js +1 -0
  58. recce/data/_next/static/chunks/turbopack-1fad664f62979b93.js +3 -0
  59. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  60. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  61. recce/data/_next/static/media/{montserrat-cyrillic-800-normal.bd5c9f50.woff → montserrat-cyrillic-800-normal.f9d58125.woff} +0 -0
  62. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  63. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  64. recce/data/_next/static/media/{montserrat-latin-800-normal.fc315020.woff → montserrat-latin-800-normal.d5761935.woff} +0 -0
  65. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  66. recce/data/_next/static/media/{montserrat-latin-ext-800-normal.2e5381b2.woff → montserrat-latin-ext-800-normal.b671449b.woff} +0 -0
  67. recce/data/_next/static/media/{montserrat-vietnamese-800-normal.20c545e6.woff → montserrat-vietnamese-800-normal.9f7b8541.woff} +0 -0
  68. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  69. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_buildManifest.js +11 -0
  70. recce/data/_next/static/nX-Uz0AH6Tc6hIQUFGqaB/_clientMiddlewareManifest.json +1 -0
  71. recce/data/_not-found/__next._full.txt +24 -0
  72. recce/data/_not-found/__next._head.txt +8 -0
  73. recce/data/_not-found/__next._index.txt +13 -0
  74. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  75. recce/data/_not-found/__next._not-found.txt +4 -0
  76. recce/data/_not-found/__next._tree.txt +6 -0
  77. recce/data/_not-found/index.html +2 -0
  78. recce/data/_not-found/index.txt +24 -0
  79. recce/data/auth_callback.html +1 -1
  80. recce/data/checks/__next.@lineage.__DEFAULT__.txt +7 -0
  81. recce/data/checks/__next._full.txt +39 -0
  82. recce/data/checks/__next._head.txt +8 -0
  83. recce/data/checks/__next._index.txt +14 -0
  84. recce/data/checks/__next._tree.txt +8 -0
  85. recce/data/checks/__next.checks.__PAGE__.txt +10 -0
  86. recce/data/checks/__next.checks.txt +4 -0
  87. recce/data/checks/index.html +2 -0
  88. recce/data/checks/index.txt +39 -0
  89. recce/data/index.html +2 -27
  90. recce/data/index.txt +32 -8
  91. recce/data/lineage/__next.@lineage.__DEFAULT__.txt +7 -0
  92. recce/data/lineage/__next._full.txt +39 -0
  93. recce/data/lineage/__next._head.txt +8 -0
  94. recce/data/lineage/__next._index.txt +14 -0
  95. recce/data/lineage/__next._tree.txt +8 -0
  96. recce/data/lineage/__next.lineage.__PAGE__.txt +10 -0
  97. recce/data/lineage/__next.lineage.txt +4 -0
  98. recce/data/lineage/index.html +2 -0
  99. recce/data/lineage/index.txt +39 -0
  100. recce/data/query/__next.@lineage.__DEFAULT__.txt +7 -0
  101. recce/data/query/__next._full.txt +37 -0
  102. recce/data/query/__next._head.txt +8 -0
  103. recce/data/query/__next._index.txt +14 -0
  104. recce/data/query/__next._tree.txt +8 -0
  105. recce/data/query/__next.query.__PAGE__.txt +9 -0
  106. recce/data/query/__next.query.txt +4 -0
  107. recce/data/query/index.html +2 -0
  108. recce/data/query/index.txt +37 -0
  109. recce/event/CONFIG.bak +1 -0
  110. recce/event/__init__.py +9 -8
  111. recce/event/collector.py +6 -2
  112. recce/event/track.py +10 -0
  113. recce/github.py +1 -1
  114. recce/mcp_server.py +725 -0
  115. recce/models/check.py +433 -15
  116. recce/models/types.py +61 -2
  117. recce/pull_request.py +1 -1
  118. recce/run.py +37 -17
  119. recce/server.py +216 -21
  120. recce/state/__init__.py +31 -0
  121. recce/state/cloud.py +644 -0
  122. recce/state/const.py +26 -0
  123. recce/state/local.py +56 -0
  124. recce/state/state.py +119 -0
  125. recce/state/state_loader.py +174 -0
  126. recce/summary.py +25 -3
  127. recce/tasks/dataframe.py +63 -1
  128. recce/tasks/query.py +40 -3
  129. recce/tasks/rowcount.py +4 -1
  130. recce/tasks/schema.py +4 -1
  131. recce/tasks/utils.py +147 -0
  132. recce/tasks/valuediff.py +85 -57
  133. recce/util/api_token.py +11 -2
  134. recce/util/breaking.py +10 -1
  135. recce/util/cll.py +1 -2
  136. recce/util/cloud/__init__.py +15 -0
  137. recce/util/cloud/base.py +115 -0
  138. recce/util/cloud/check_events.py +190 -0
  139. recce/util/cloud/checks.py +242 -0
  140. recce/util/io.py +2 -2
  141. recce/util/lineage.py +19 -18
  142. recce/util/perf_tracking.py +85 -0
  143. recce/util/recce_cloud.py +254 -5
  144. recce/util/startup_perf.py +121 -0
  145. recce/yaml/__init__.py +2 -2
  146. {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/METADATA +91 -71
  147. recce_nightly-1.30.0.20251221.dist-info/RECORD +183 -0
  148. {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/WHEEL +1 -2
  149. recce/data/_next/static/abCX3x3UoIdRLEDWxx4xd/_buildManifest.js +0 -1
  150. recce/data/_next/static/chunks/181-acc61ddada3bc0ca.js +0 -43
  151. recce/data/_next/static/chunks/1bff33f1-1ef85cf5e658a751.js +0 -1
  152. recce/data/_next/static/chunks/217-879a84d70f7a907c.js +0 -2
  153. recce/data/_next/static/chunks/29e3cc0d-60045b2e47aa3916.js +0 -1
  154. recce/data/_next/static/chunks/36e1c10d-8e7be4a6c1f6ab2d.js +0 -1
  155. recce/data/_next/static/chunks/3998a672-03adacad07b346ac.js +0 -1
  156. recce/data/_next/static/chunks/3a92ee20-1081c360214f9602.js +0 -1
  157. recce/data/_next/static/chunks/42-cd3c06533f5fd47c.js +0 -9
  158. recce/data/_next/static/chunks/450c323b-fd94e7ffaa4a5efa.js +0 -1
  159. recce/data/_next/static/chunks/47d8844f-929aed9b1c73a905.js +0 -1
  160. recce/data/_next/static/chunks/608-3b079b544e5d5f5e.js +0 -15
  161. recce/data/_next/static/chunks/6dc81886-adbfa45836061d79.js +0 -1
  162. recce/data/_next/static/chunks/7a8a3e83-edf6dc64b5d5f0a5.js +0 -1
  163. recce/data/_next/static/chunks/7f27ae6c-d5f0438edd5c2a5b.js +0 -1
  164. recce/data/_next/static/chunks/86730205-cfb14e3f051bab35.js +0 -1
  165. recce/data/_next/static/chunks/8d700b6a.8bb140898499c512.js +0 -1
  166. recce/data/_next/static/chunks/92-607cd1af83c41f43.js +0 -1
  167. recce/data/_next/static/chunks/9746af58-a42b7d169cacadf0.js +0 -1
  168. recce/data/_next/static/chunks/a30376cd-de84559016d7e133.js +0 -1
  169. recce/data/_next/static/chunks/app/_not-found/page-01ed58b7f971d311.js +0 -1
  170. recce/data/_next/static/chunks/app/layout-177a410a97e0d018.js +0 -1
  171. recce/data/_next/static/chunks/app/page-da6e046a8235dbfc.js +0 -1
  172. recce/data/_next/static/chunks/b63b1b3f-4282bdcf459e075c.js +0 -1
  173. recce/data/_next/static/chunks/bbda5537-9ec25eb1dd62348a.js +0 -1
  174. recce/data/_next/static/chunks/c132bf7d-08cb668a789d6afd.js +0 -1
  175. recce/data/_next/static/chunks/ce84277d-2e5d1d46910cf052.js +0 -1
  176. recce/data/_next/static/chunks/febdd86e-c6b525341634b860.js +0 -54
  177. recce/data/_next/static/chunks/fee69bc6-2dbccaf9b90474e6.js +0 -1
  178. recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
  179. recce/data/_next/static/chunks/main-app-39061b0166c47f55.js +0 -1
  180. recce/data/_next/static/chunks/main-b5b3ae20a1405261.js +0 -1
  181. recce/data/_next/static/chunks/pages/_app-437c455677d62394.js +0 -1
  182. recce/data/_next/static/chunks/pages/_error-e7650df18ca04bde.js +0 -1
  183. recce/data/_next/static/chunks/webpack-7b49d5ba7e3a434d.js +0 -1
  184. recce/data/_next/static/css/17a96168e3a9db13.css +0 -1
  185. recce/data/_next/static/css/1b121dc4d36aeb4d.css +0 -3
  186. recce/data/_next/static/css/35c6679a098e1e34.css +0 -1
  187. recce/data/_next/static/css/951e2e0eea2d4a5b.css +0 -14
  188. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  189. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  190. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  191. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  192. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  193. recce/data/_next/static/media/reload-image.79aabb7d.svg +0 -4
  194. recce/state.py +0 -786
  195. recce_nightly-1.10.0.20250625.dist-info/RECORD +0 -154
  196. recce_nightly-1.10.0.20250625.dist-info/top_level.txt +0 -2
  197. tests/__init__.py +0 -0
  198. tests/adapter/__init__.py +0 -0
  199. tests/adapter/dbt_adapter/__init__.py +0 -0
  200. tests/adapter/dbt_adapter/conftest.py +0 -17
  201. tests/adapter/dbt_adapter/dbt_test_helper.py +0 -298
  202. tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -25
  203. tests/adapter/dbt_adapter/test_dbt_cll.py +0 -384
  204. tests/adapter/dbt_adapter/test_selector.py +0 -202
  205. tests/tasks/__init__.py +0 -0
  206. tests/tasks/conftest.py +0 -4
  207. tests/tasks/test_histogram.py +0 -129
  208. tests/tasks/test_lineage.py +0 -55
  209. tests/tasks/test_preset_checks.py +0 -64
  210. tests/tasks/test_profile.py +0 -397
  211. tests/tasks/test_query.py +0 -151
  212. tests/tasks/test_row_count.py +0 -135
  213. tests/tasks/test_schema.py +0 -122
  214. tests/tasks/test_top_k.py +0 -77
  215. tests/tasks/test_valuediff.py +0 -85
  216. tests/test_cli.py +0 -133
  217. tests/test_config.py +0 -43
  218. tests/test_connect_to_cloud.py +0 -82
  219. tests/test_core.py +0 -29
  220. tests/test_dbt.py +0 -36
  221. tests/test_pull_request.py +0 -130
  222. tests/test_server.py +0 -104
  223. tests/test_state.py +0 -134
  224. tests/test_summary.py +0 -65
  225. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  226. /recce/data/_next/static/media/{montserrat-cyrillic-ext-800-normal.e6e0d8d0.woff → montserrat-cyrillic-ext-800-normal.a4fa76b5.woff} +0 -0
  227. /recce/data/_next/static/{abCX3x3UoIdRLEDWxx4xd → nX-Uz0AH6Tc6hIQUFGqaB}/_ssgManifest.js +0 -0
  228. {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/entry_points.txt +0 -0
  229. {recce_nightly-1.10.0.20250625.dist-info → recce_nightly-1.30.0.20251221.dist-info}/licenses/LICENSE +0 -0
tests/test_config.py DELETED
@@ -1,43 +0,0 @@
1
- import os.path
2
- from unittest import TestCase
3
- from unittest.mock import patch
4
-
5
- from recce.config import RecceConfig
6
- from recce.util import SingletonMeta
7
-
8
- test_root_path = os.path.dirname(os.path.abspath(__file__))
9
-
10
-
11
- class RecceConfigTestCase(TestCase):
12
- def setUp(self):
13
- self.recce_config_path = os.path.join(test_root_path, "data", "config", "recce.yml")
14
- pass
15
-
16
- def tearDown(self):
17
- # Reset the SingletonMeta instances due to RecceConfig is a singleton
18
- SingletonMeta._instances = {}
19
-
20
- def test_load_recce_config(self):
21
- config = RecceConfig(self.recce_config_path)
22
-
23
- # Test data contains 2 checks
24
- preset_checks = config.config.get("checks")
25
- self.assertIsNotNone(preset_checks)
26
- self.assertIsInstance(preset_checks, list)
27
- self.assertEqual(len(preset_checks), 2)
28
-
29
- @patch("recce.config.RecceConfig.save")
30
- def test_recce_config_not_found(self, mock_save):
31
- default_config = RecceConfig("NOT_EXISTING_FILE")
32
- assert mock_save.called is True
33
- # Default config should be generated
34
- preset_checks = default_config.config.get("checks")
35
- self.assertIsNotNone(default_config.config)
36
- self.assertIsInstance(preset_checks, list)
37
- self.assertEqual(len(preset_checks), 2)
38
-
39
- @patch("recce.yaml.safe_load")
40
- def test_recce_config_null_checks(self, mock_yaml_safe_load):
41
- # mock to load a yaml file with null checks
42
- mock_yaml_safe_load.return_value = {"checks": None}
43
- RecceConfig(self.recce_config_path)
@@ -1,82 +0,0 @@
1
- import base64
2
- import unittest
3
- from unittest.mock import patch
4
- from urllib.parse import quote
5
-
6
- from cryptography.hazmat.primitives import hashes
7
- from cryptography.hazmat.primitives.asymmetric import padding
8
-
9
- from recce.connect_to_cloud import (
10
- connect_to_cloud_background_task,
11
- decrypt_code,
12
- generate_key_pair,
13
- is_callback_server_running,
14
- prepare_connection_url,
15
- )
16
-
17
-
18
- class ConnectToCloudTests(unittest.TestCase):
19
-
20
- def test_generate_key_pair(self):
21
- private_key, public_key = generate_key_pair()
22
- self.assertIsNotNone(private_key)
23
- self.assertIsNotNone(public_key)
24
- self.assertEqual(private_key.public_key().public_numbers(), public_key.public_numbers())
25
-
26
- def test_prepare_connection_url(self):
27
- _, public_key = generate_key_pair()
28
- url, port = prepare_connection_url(public_key)
29
- self.assertIn("connect?", url)
30
- self.assertTrue(port >= 10000 and port <= 15000)
31
-
32
- def test_decrypt_code(self):
33
- private_key, public_key = generate_key_pair()
34
- test_string = "recce-api-token-123"
35
- ciphertext = public_key.encrypt(
36
- test_string.encode(),
37
- padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA1()), algorithm=hashes.SHA1(), label=None),
38
- )
39
- b64_ciphertext = base64.b64encode(ciphertext).decode()
40
- result = decrypt_code(private_key, b64_ciphertext)
41
- self.assertEqual(result, test_string)
42
-
43
- @patch("recce.connect_to_cloud.update_recce_api_token")
44
- @patch("recce.connect_to_cloud.update_onboarding_state")
45
- @patch("recce.connect_to_cloud.RecceCloud")
46
- def test_handle_callback_request_success(self, mock_recce_cloud, mock_update_state, mock_update_token):
47
- private_key, public_key = generate_key_pair()
48
-
49
- # Prepare encrypted token
50
- test_token = "recce-api-token-xyz"
51
- ciphertext = public_key.encrypt(
52
- test_token.encode(),
53
- padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA1()), algorithm=hashes.SHA1(), label=None),
54
- )
55
- encrypted_b64 = base64.b64encode(ciphertext).decode()
56
-
57
- # Set up mocks
58
- mock_recce_cloud.return_value.verify_token.return_value = True
59
-
60
- from recce.connect_to_cloud import handle_callback_request
61
-
62
- result = handle_callback_request(f"code={quote(encrypted_b64)}", private_key)
63
-
64
- assert result == test_token
65
- mock_update_token.assert_called_once_with(test_token)
66
- mock_update_state.assert_called_once_with(test_token, False)
67
-
68
- def test_is_callback_server_running(self):
69
- # Should return False by default
70
- self.assertFalse(is_callback_server_running())
71
-
72
- @patch("recce.connect_to_cloud.run_one_time_http_server")
73
- def test_connect_to_cloud_background_task_runs(self, mock_server):
74
- private_key, public_key = generate_key_pair()
75
- url, port = prepare_connection_url(public_key)
76
-
77
- connect_to_cloud_background_task(private_key, port, url)
78
- mock_server.assert_called_once()
79
-
80
-
81
- if __name__ == "__main__":
82
- unittest.main()
tests/test_core.py DELETED
@@ -1,29 +0,0 @@
1
- # noinspection PyUnresolvedReferences
2
- from tests.adapter.dbt_adapter.conftest import dbt_test_helper # noqa: F401
3
-
4
-
5
- def test_lineage_diff(dbt_test_helper):
6
- sql_model1 = """
7
- select a from T
8
- """
9
-
10
- sql_model2 = """
11
- select a from {{ ref("model1") }}
12
- """
13
-
14
- sql_model2_ = """
15
- select
16
- a,b
17
- from
18
- {{ ref("model1") }}
19
- """
20
-
21
- dbt_test_helper.create_model("model1", sql_model1, sql_model1)
22
- dbt_test_helper.create_model("model2", sql_model2, sql_model2_)
23
- result = dbt_test_helper.context.get_lineage_diff()
24
- nodediff = result.diff.get("model1")
25
- assert nodediff is None
26
- nodediff2 = result.diff.get("model2")
27
- assert (
28
- nodediff2 is not None and nodediff2.change_status == "modified" and nodediff2.change.category == "non_breaking"
29
- )
tests/test_dbt.py DELETED
@@ -1,36 +0,0 @@
1
- import os
2
- from unittest import TestCase
3
- from unittest.mock import MagicMock
4
-
5
- from recce.adapter.dbt_adapter import DbtAdapter, load_catalog, load_manifest
6
-
7
- current_dir = os.path.dirname(os.path.abspath(__file__))
8
-
9
-
10
- class TestAdapterLineage(TestCase):
11
- def setUp(self) -> None:
12
- self.manifest = load_manifest(path=os.path.join(current_dir, "manifest.json"))
13
- assert self.manifest is not None
14
-
15
- self.catalog = load_catalog(path=os.path.join(current_dir, "catalog.json"))
16
- assert self.catalog is not None
17
-
18
- def tearDown(self):
19
- pass
20
-
21
- def test_load_lineage(self):
22
- dbt_adapter = DbtAdapter(curr_manifest=self.manifest)
23
- lineage = dbt_adapter.get_lineage()
24
- assert lineage is not None
25
- assert lineage["nodes"]["model.jaffle_shop.orders"] is not None
26
- assert "columns" not in lineage["nodes"]["model.jaffle_shop.orders"]
27
-
28
- def test_load_lineage_with_catalog(self):
29
- mock_adapter = MagicMock()
30
- mock_adapter.type.return_value = None
31
-
32
- dbt_adapter = DbtAdapter(curr_manifest=self.manifest, curr_catalog=self.catalog)
33
- dbt_adapter.adapter = mock_adapter
34
- lineage = dbt_adapter.get_lineage()
35
- assert lineage is not None
36
- assert len(lineage["nodes"]["model.jaffle_shop.orders"]["columns"]) == 9
@@ -1,130 +0,0 @@
1
- import json
2
- from unittest.mock import patch
3
-
4
- import pytest
5
- import requests
6
-
7
- from recce.pull_request import _fetch_pr_title, fetch_pr_metadata_from_event_path
8
-
9
-
10
- @pytest.fixture
11
- def mock_github_event(tmp_path, monkeypatch):
12
- event_data = {
13
- "number": 1,
14
- "pull_request": {
15
- "_links": {
16
- "html": {"href": "https://github.com/xyz/abc/pull/1"},
17
- "self": {"href": "api_url_here"},
18
- }
19
- },
20
- }
21
- event_file = tmp_path / "github_event.json"
22
- with open(event_file, "w") as f:
23
- json.dump(event_data, f)
24
-
25
- monkeypatch.setenv("GITHUB_EVENT_PATH", str(event_file))
26
- monkeypatch.setenv("GITHUB_REPOSITORY", "abc/xyz")
27
-
28
-
29
- @pytest.fixture
30
- def mock_github_token(monkeypatch):
31
- monkeypatch.setenv("GITHUB_TOKEN", "github_token_here")
32
-
33
-
34
- class MockResponse:
35
- def __init__(self, status_code, json_data=None):
36
- self.status_code = status_code
37
- self.json_data = json_data
38
-
39
- def json(self):
40
- return self.json_data
41
-
42
-
43
- @pytest.fixture
44
- def mock_get_request_success(monkeypatch):
45
- def mock_get(*args, **kwargs):
46
- return MockResponse(200, json_data={"title": "Update README.md"})
47
-
48
- monkeypatch.setattr(requests, "get", mock_get)
49
-
50
-
51
- @pytest.fixture
52
- def mock_get_request_failure(monkeypatch):
53
- def mock_get(*args, **kwargs):
54
- return MockResponse(404)
55
-
56
- monkeypatch.setattr(requests, "get", mock_get)
57
-
58
-
59
- def test_fetch_pr_metadata(mock_github_event, mock_github_token, mock_get_request_success):
60
- result = fetch_pr_metadata_from_event_path()
61
-
62
- assert result is not None
63
- assert result["github_pr_id"] == 1
64
- assert result["github_pr_url"] == "https://github.com/xyz/abc/pull/1"
65
- assert result["github_pr_title"] == "Update README.md"
66
- assert result["github_repository"] == "abc/xyz"
67
-
68
-
69
- def test_fetch_pr_metadata_no_event(mock_get_request_success, monkeypatch):
70
- monkeypatch.delenv("GITHUB_EVENT_PATH", raising=False)
71
-
72
- result = fetch_pr_metadata_from_event_path()
73
-
74
- assert result is None
75
-
76
-
77
- def test_fetch_pr_metadata_request_error(mock_github_event, mock_get_request_failure):
78
- result = fetch_pr_metadata_from_event_path()
79
-
80
- assert result is not None
81
- assert result["github_pr_id"] == 1
82
- assert result["github_pr_url"] == "https://github.com/xyz/abc/pull/1"
83
- assert result["github_pr_title"] is None
84
-
85
-
86
- def test_fetch_pr_metadata_exception(mock_github_event, monkeypatch):
87
- monkeypatch.setenv("GITHUB_EVENT_PATH", "nonexistent_path")
88
-
89
- result = fetch_pr_metadata_from_event_path()
90
-
91
- assert result is None
92
-
93
-
94
- def test_fetch_pr_metadata_no_token(mock_github_event):
95
- with patch.dict("os.environ", clear=True):
96
- result = fetch_pr_metadata_from_event_path()
97
-
98
- assert result is None
99
-
100
-
101
- def test_fetch_pr_title(mock_github_token, mock_get_request_success):
102
- result = _fetch_pr_title("api_url_here")
103
-
104
- assert result == "Update README.md"
105
-
106
-
107
- def test_fetch_pr_title_request_error(mock_github_token, mock_get_request_failure):
108
- result = _fetch_pr_title("api_url_here")
109
-
110
- assert result is None
111
-
112
-
113
- def test_fetch_pr_title_exception(mock_github_token, monkeypatch):
114
- monkeypatch.setenv("GITHUB_TOKEN", "github_token_here")
115
-
116
- def mock_get(*args, **kwargs):
117
- raise Exception("Test exception")
118
-
119
- monkeypatch.setattr(requests, "get", mock_get)
120
-
121
- result = _fetch_pr_title("api_url_here")
122
-
123
- assert result is None
124
-
125
-
126
- def test_fetch_pr_title_no_token():
127
- with patch.dict("os.environ", clear=True):
128
- result = _fetch_pr_title("api_url_here")
129
-
130
- assert result is None
tests/test_server.py DELETED
@@ -1,104 +0,0 @@
1
- import os
2
-
3
- import pytest
4
- from fastapi.testclient import TestClient
5
-
6
- from recce.core import default_context
7
- from recce.server import app
8
-
9
- # noinspection PyUnresolvedReferences
10
- from tests.adapter.dbt_adapter.conftest import dbt_test_helper # noqa: F401
11
-
12
-
13
- @pytest.fixture
14
- def temp_folder():
15
- import tempfile
16
-
17
- temp_dir = tempfile.mkdtemp()
18
- yield temp_dir
19
-
20
- import shutil
21
-
22
- shutil.rmtree(temp_dir)
23
-
24
-
25
- def test_health():
26
- client = TestClient(app)
27
- response = client.get("/api/health")
28
- assert response.status_code == 200
29
- assert response.json() == {"status": "ok"}
30
-
31
-
32
- def test_stateless(dbt_test_helper):
33
- context = default_context()
34
- from recce.state import RecceStateLoader
35
-
36
- context.state_loader = RecceStateLoader()
37
- client = TestClient(app)
38
- response = client.get("/api/info")
39
- assert response.status_code == 200
40
- info = response.json()
41
- assert info["file_mode"] is False
42
- assert info["cloud_mode"] is False
43
-
44
-
45
- def test_file_mode(dbt_test_helper):
46
- context = default_context()
47
- from recce.state import RecceStateLoader
48
-
49
- context.state_loader = RecceStateLoader(state_file="/tmp/recce_state.json")
50
- client = TestClient(app)
51
- response = client.get("/api/info")
52
- assert response.status_code == 200
53
- info = response.json()
54
- assert info["file_mode"] is True
55
- assert info["filename"] == "recce_state.json"
56
- assert info["cloud_mode"] is False
57
-
58
-
59
- def test_saveas_and_rename(dbt_test_helper, temp_folder):
60
- context = default_context()
61
- state_file = os.path.join(temp_folder, "recce_state.json")
62
- state_file2 = os.path.join(temp_folder, "recce_state2.json")
63
- state_file3 = os.path.join(temp_folder, "recce_state3.json")
64
- os.makedirs(os.path.join(temp_folder, "dir.json"))
65
-
66
- from recce.state import RecceStateLoader
67
-
68
- context.state_loader = RecceStateLoader(state_file=state_file)
69
- client = TestClient(app)
70
-
71
- response = client.post("/api/save", json={"filename": "recce_state2.json"})
72
- assert response.status_code == 200
73
- assert os.path.exists(state_file)
74
-
75
- response = client.post("/api/save-as", json={"filename": "recce_state2.json"})
76
- assert response.status_code == 200
77
- assert os.path.exists(state_file2)
78
- assert context.state_loader.state_file == os.path.join(temp_folder, "recce_state2.json")
79
-
80
- # Same file
81
- response = client.post("/api/save-as", json={"filename": "recce_state2.json"})
82
- assert response.status_code == 400
83
-
84
- # folder
85
- response = client.post("/api/save-as", json={"filename": "dir.json"})
86
- assert response.status_code == 400
87
-
88
- # Rename
89
- response = client.post("/api/rename", json={"filename": "recce_state3.json"})
90
- assert response.status_code == 200
91
- assert not os.path.exists(state_file2)
92
- assert os.path.exists(state_file3)
93
- assert context.state_loader.state_file == os.path.join(temp_folder, "recce_state3.json")
94
-
95
- # Conflict
96
- response = client.post("/api/save-as", json={"filename": "recce_state.json"})
97
- assert response.status_code == 409
98
- response = client.post("/api/rename", json={"filename": "recce_state.json"})
99
- assert response.status_code == 409
100
-
101
- # Overwrite
102
- response = client.post("/api/save-as", json={"filename": "recce_state.json", "overwrite": True})
103
- assert response.status_code == 200
104
- assert context.state_loader.state_file == os.path.join(temp_folder, "recce_state.json")
tests/test_state.py DELETED
@@ -1,134 +0,0 @@
1
- # add the RecceState test case
2
- import os
3
- import unittest
4
- from datetime import datetime
5
-
6
- from recce.core import RecceContext
7
- from recce.models import Check, Run
8
- from recce.state import ArtifactsRoot, RecceState, RecceStateLoader
9
-
10
- current_dir = os.path.dirname(os.path.abspath(__file__))
11
-
12
-
13
- class TestRecceState(unittest.TestCase):
14
- def test_merge_checks(self):
15
- check1 = Check(name="test1", description="", type="query")
16
- check2 = Check(name="test2", description="", type="query", updated_at=datetime(2000, 1, 1))
17
- check2_2 = Check(
18
- name="test2_2", description="", type="query", updated_at=datetime(2020, 1, 1), check_id=check2.check_id
19
- )
20
- check3 = Check(name="test3", description="", type="query")
21
-
22
- context = RecceContext()
23
- state = RecceState(checks=[check1], runs=[])
24
- context.import_state(state)
25
- self.assertEqual(1, len(context.checks))
26
- self.assertEqual(check1.name, context.checks[0].name)
27
-
28
- context = RecceContext(checks=[check1, check2])
29
- state = RecceState(checks=[check1, check2_2, check3], runs=[])
30
- context.import_state(state)
31
- self.assertEqual(3, len(context.checks))
32
- self.assertEqual(check2_2.name, context.checks[1].name)
33
-
34
- def test_merge_preset_checks(self):
35
- check1 = Check(
36
- name="test1",
37
- description="test1",
38
- type="query",
39
- params=dict(foo="bar"),
40
- updated_at=datetime(2000, 1, 1),
41
- is_preset=True,
42
- )
43
- check2 = Check(
44
- name="test2",
45
- description="test2",
46
- type="query",
47
- params=dict(foo="bar"),
48
- updated_at=datetime(2001, 1, 1),
49
- is_preset=True,
50
- )
51
-
52
- context = RecceContext(checks=[check1])
53
- state = RecceState(checks=[check2], runs=[])
54
- context.import_state(state)
55
- self.assertEqual(1, len(context.checks))
56
- self.assertEqual(check2.name, context.checks[0].name)
57
-
58
- context = RecceContext(checks=[check2])
59
- state = RecceState(checks=[check1], runs=[])
60
- context.import_state(state)
61
- self.assertEqual(1, len(context.checks))
62
- self.assertEqual(check2.name, context.checks[0].name)
63
-
64
- def test_revert_checks(self):
65
- check1 = Check(name="test1", description="", type="query")
66
- check2 = Check(name="test2", description="", type="query")
67
- check2_2 = Check(name="test2_2", description="", type="query", check_id=check2.check_id)
68
- check3 = Check(name="test3", description="", type="query")
69
-
70
- context = RecceContext(checks=[check1, check2])
71
- state = RecceState(checks=[check2_2, check3], runs=[])
72
- context.import_state(state, merge=False)
73
- self.assertEqual(2, len(context.checks))
74
- self.assertEqual(check2_2.name, context.checks[0].name)
75
-
76
- def test_merge_runs(self):
77
- run1 = Run(type="query")
78
- run2 = Run(type="query")
79
- run3 = Run(type="query")
80
-
81
- context = RecceContext(runs=[])
82
- state = RecceState(runs=[run1])
83
- context.import_state(state)
84
- self.assertEqual(1, len(context.runs))
85
-
86
- context = RecceContext(runs=[run1, run2])
87
- state = RecceState(runs=[run2, run3])
88
- context.import_state(state)
89
- self.assertEqual(3, len(context.runs))
90
-
91
- def test_merge_dbt_artifacts(self):
92
- import json
93
- import os
94
-
95
- with open(os.path.join(current_dir, "manifest.json"), "r") as f:
96
- manifest = json.load(f)
97
- manifest["metadata"]["generated_at"] = "2000-01-01T00:00:00Z"
98
- artifacts = ArtifactsRoot(
99
- base=dict(
100
- manifest=manifest,
101
- ),
102
- current=dict(
103
- manifest=manifest,
104
- ),
105
- )
106
-
107
- from tests.adapter.dbt_adapter.dbt_test_helper import DbtTestHelper
108
-
109
- adapter = DbtTestHelper().adapter
110
- adapter.import_artifacts(artifacts)
111
- self.assertNotEqual(adapter.base_manifest.metadata.invocation_id, manifest.get("metadata").get("invocation_id"))
112
-
113
- manifest["metadata"]["generated_at"] = "2099-01-01T00:00:00Z"
114
- adapter.import_artifacts(artifacts)
115
- self.assertEqual(adapter.base_manifest.metadata.invocation_id, manifest.get("metadata").get("invocation_id"))
116
-
117
- def test_state_loader(self):
118
- # copy ./recce_state.json to temp and open
119
-
120
- # use library to create a temp file in the context
121
- import os
122
- import shutil
123
- import tempfile
124
-
125
- with tempfile.NamedTemporaryFile() as f:
126
- # copy ./recce_state.json to temp file
127
- current_dir = os.path.dirname(os.path.abspath(__file__))
128
- state_file = os.path.join(current_dir, "recce_state.json")
129
- shutil.copy(state_file, f.name)
130
-
131
- # load the state file
132
- state_loader = RecceStateLoader(state_file=f.name)
133
- state = state_loader.load()
134
- assert len(state.runs) == 17
tests/test_summary.py DELETED
@@ -1,65 +0,0 @@
1
- import os
2
- import unittest
3
-
4
- from recce.adapter.dbt_adapter import DbtAdapter, DbtVersion, load_manifest
5
- from recce.core import RecceContext, set_default_context
6
- from recce.summary import (
7
- _build_lineage_graph,
8
- generate_mermaid_lineage_graph,
9
- generate_summary_metadata,
10
- )
11
-
12
- current_dir = os.path.dirname(os.path.abspath(__file__))
13
- base_manifest_dir = os.path.join(current_dir, "data", "manifest", "base")
14
- pr2_manifest_dir = os.path.join(current_dir, "data", "manifest", "pr2") # Pull Request 2l
15
-
16
- dbt_version = DbtVersion()
17
-
18
-
19
- @unittest.skipIf(dbt_version < "1.8.1", "Dbt version is less than 1.8.1")
20
- def test_generate_summary_metadata():
21
- manifest = load_manifest(path=os.path.join(current_dir, "manifest.json"))
22
- assert manifest is not None
23
- dbt_adapter = DbtAdapter(curr_manifest=manifest)
24
- curr_lineage = dbt_adapter.get_lineage()
25
- base_lineage = dbt_adapter.get_lineage()
26
-
27
- # Summary with no changes
28
- generate_summary_metadata(curr_lineage, base_lineage)
29
-
30
- base_manifest = load_manifest(path=os.path.join(base_manifest_dir, "manifest.json"))
31
- curr_manifest = load_manifest(path=os.path.join(pr2_manifest_dir, "manifest.json"))
32
- dbt_adapter = DbtAdapter(curr_manifest=curr_manifest, base_manifest=base_manifest)
33
- curr_lineage = dbt_adapter.get_lineage()
34
- base_lineage = dbt_adapter.get_lineage(base=True)
35
- generate_summary_metadata(curr_lineage, base_lineage)
36
-
37
-
38
- @unittest.skipIf(dbt_version < "v1.8.1", "Dbt version is less than 1.8.1")
39
- def test_build_lineage_graph():
40
- base_manifest = load_manifest(path=os.path.join(base_manifest_dir, "manifest.json"))
41
- curr_manifest = load_manifest(path=os.path.join(pr2_manifest_dir, "manifest.json"))
42
- dbt_adapter = DbtAdapter(curr_manifest=curr_manifest, base_manifest=base_manifest)
43
- curr_lineage = dbt_adapter.get_lineage()
44
- base_lineage = dbt_adapter.get_lineage(base=True)
45
-
46
- lineage_graph = _build_lineage_graph(curr_lineage, base_lineage)
47
- # Modified nodes: 3
48
- # - int_customer_orders: added
49
- # - int_customer_payments: added
50
- # - customers: modified
51
- assert len(lineage_graph.modified_set) == 3
52
-
53
-
54
- @unittest.skipIf(dbt_version < "v1.8.1", "Dbt version is less than 1.8.1")
55
- def test_generate_mermaid_lineage_graph():
56
- set_default_context(RecceContext())
57
- base_manifest = load_manifest(path=os.path.join(base_manifest_dir, "manifest.json"))
58
- curr_manifest = load_manifest(path=os.path.join(pr2_manifest_dir, "manifest.json"))
59
- dbt_adapter = DbtAdapter(curr_manifest=curr_manifest, base_manifest=base_manifest)
60
- curr_lineage = dbt_adapter.get_lineage()
61
- base_lineage = dbt_adapter.get_lineage(base=True)
62
- graph = _build_lineage_graph(curr_lineage, base_lineage)
63
- mermaid_content, is_empty_graph, is_partial_graph = generate_mermaid_lineage_graph(graph)
64
- assert is_empty_graph is False
65
- assert is_partial_graph is False