recce-nightly 1.15.0.20250806__py3-none-any.whl → 1.26.0.20251124__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 (167) hide show
  1. recce/VERSION +1 -1
  2. recce/__init__.py +5 -0
  3. recce/adapter/dbt_adapter/__init__.py +12 -3
  4. recce/artifact.py +74 -1
  5. recce/cli.py +642 -101
  6. recce/config.py +2 -2
  7. recce/connect_to_cloud.py +1 -1
  8. recce/core.py +2 -2
  9. recce/data/404.html +1 -1
  10. recce/data/__next.__PAGE__.txt +10 -0
  11. recce/data/__next._full.txt +23 -0
  12. recce/data/__next._head.txt +8 -0
  13. recce/data/__next._index.txt +8 -0
  14. recce/data/__next._tree.txt +5 -0
  15. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
  16. recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
  17. recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
  18. recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
  19. recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
  20. recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
  21. recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
  22. recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
  23. recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
  24. recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
  25. recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
  26. recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
  27. recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
  28. recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
  29. recce/data/_next/static/chunks/99d638224186c118.js +1 -0
  30. recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
  31. recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
  32. recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
  33. recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
  34. recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
  35. recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.js +3 -0
  36. recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
  37. recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
  38. recce/data/_next/static/media/{montserrat-cyrillic-800-normal.bd5c9f50.woff → montserrat-cyrillic-800-normal.f9d58125.woff} +0 -0
  39. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
  40. recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
  41. recce/data/_next/static/media/{montserrat-latin-800-normal.fc315020.woff → montserrat-latin-800-normal.d5761935.woff} +0 -0
  42. recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
  43. recce/data/_next/static/media/{montserrat-latin-ext-800-normal.2e5381b2.woff → montserrat-latin-ext-800-normal.b671449b.woff} +0 -0
  44. recce/data/_next/static/media/{montserrat-vietnamese-800-normal.20c545e6.woff → montserrat-vietnamese-800-normal.9f7b8541.woff} +0 -0
  45. recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
  46. recce/data/_not-found/__next._full.txt +17 -0
  47. recce/data/_not-found/__next._head.txt +8 -0
  48. recce/data/_not-found/__next._index.txt +8 -0
  49. recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
  50. recce/data/_not-found/__next._not-found.txt +4 -0
  51. recce/data/_not-found/__next._tree.txt +3 -0
  52. recce/data/_not-found.html +1 -0
  53. recce/data/_not-found.txt +17 -0
  54. recce/data/index.html +1 -1
  55. recce/data/index.txt +21 -23
  56. recce/event/__init__.py +9 -8
  57. recce/event/collector.py +3 -1
  58. recce/event/track.py +10 -0
  59. recce/github.py +1 -1
  60. recce/mcp_server.py +716 -0
  61. recce/models/types.py +35 -2
  62. recce/pull_request.py +1 -1
  63. recce/run.py +2 -2
  64. recce/server.py +105 -3
  65. recce/state/__init__.py +31 -0
  66. recce/state/cloud.py +632 -0
  67. recce/state/const.py +26 -0
  68. recce/state/local.py +56 -0
  69. recce/state/state.py +119 -0
  70. recce/state/state_loader.py +174 -0
  71. recce/summary.py +21 -1
  72. recce/tasks/dataframe.py +63 -1
  73. recce/tasks/rowcount.py +4 -1
  74. recce/tasks/schema.py +4 -1
  75. recce/util/api_token.py +9 -2
  76. recce/util/breaking.py +1 -1
  77. recce/util/io.py +2 -2
  78. recce/util/lineage.py +14 -18
  79. recce/util/recce_cloud.py +187 -7
  80. recce/yaml/__init__.py +2 -2
  81. recce_cloud/__init__.py +24 -0
  82. recce_cloud/api/__init__.py +17 -0
  83. recce_cloud/api/base.py +111 -0
  84. recce_cloud/api/client.py +150 -0
  85. recce_cloud/api/exceptions.py +26 -0
  86. recce_cloud/api/factory.py +63 -0
  87. recce_cloud/api/github.py +76 -0
  88. recce_cloud/api/gitlab.py +82 -0
  89. recce_cloud/artifact.py +57 -0
  90. recce_cloud/ci_providers/__init__.py +9 -0
  91. recce_cloud/ci_providers/base.py +82 -0
  92. recce_cloud/ci_providers/detector.py +147 -0
  93. recce_cloud/ci_providers/github_actions.py +136 -0
  94. recce_cloud/ci_providers/gitlab_ci.py +130 -0
  95. recce_cloud/cli.py +245 -0
  96. recce_cloud/upload.py +214 -0
  97. {recce_nightly-1.15.0.20250806.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +54 -28
  98. recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
  99. {recce_nightly-1.15.0.20250806.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
  100. tests/adapter/dbt_adapter/test_dbt_cll.py +4 -2
  101. tests/recce_cloud/__init__.py +0 -0
  102. tests/recce_cloud/test_ci_providers.py +351 -0
  103. tests/recce_cloud/test_cli.py +372 -0
  104. tests/recce_cloud/test_client.py +273 -0
  105. tests/recce_cloud/test_platform_clients.py +333 -0
  106. tests/test_cli.py +106 -3
  107. tests/test_cli_mcp_optional.py +45 -0
  108. tests/test_cloud_listing_cli.py +324 -0
  109. tests/test_core.py +147 -0
  110. tests/test_mcp_server.py +332 -0
  111. tests/test_server.py +6 -6
  112. tests/test_summary.py +14 -6
  113. recce/data/_next/static/Q_5ThPsmamd4VAGXuqwgi/_buildManifest.js +0 -1
  114. recce/data/_next/static/chunks/0376eeba-3db2196398d62270.js +0 -1
  115. recce/data/_next/static/chunks/068b80ea-833a129468ee1622.js +0 -1
  116. recce/data/_next/static/chunks/0ddaf06c-c7961285f66460f6.js +0 -1
  117. recce/data/_next/static/chunks/1268aea1-6dc1251c01bd724b.js +0 -54
  118. recce/data/_next/static/chunks/12f8fac4-16838e42d28d45c3.js +0 -1
  119. recce/data/_next/static/chunks/235b8375-8c84c51d7bd4f6aa.js +0 -1
  120. recce/data/_next/static/chunks/2541941f-2cd3a7c2d629bd33.js +0 -1
  121. recce/data/_next/static/chunks/273-f3fa401bd2b6fc91.js +0 -10
  122. recce/data/_next/static/chunks/2fc37c1e-910deebeb3d77c90.js +0 -1
  123. recce/data/_next/static/chunks/338-2e7eed5135c64550.js +0 -30
  124. recce/data/_next/static/chunks/367-ab8b16dd5f8586ca.js +0 -1
  125. recce/data/_next/static/chunks/3a92ee20-0400ffe460c7c803.js +0 -1
  126. recce/data/_next/static/chunks/62446465-423c03bb8c1f59b6.js +0 -1
  127. recce/data/_next/static/chunks/6af7f9e9-60aa8706f49dae45.js +0 -1
  128. recce/data/_next/static/chunks/6cf54382-49d52ae6e564e2ac.js +0 -1
  129. recce/data/_next/static/chunks/6dc81886-78e2efe4538794ae.js +0 -1
  130. recce/data/_next/static/chunks/715e4acc-9e2e6df4eb3809d1.js +0 -1
  131. recce/data/_next/static/chunks/72-181b430654230f0e.js +0 -1
  132. recce/data/_next/static/chunks/786-774e3e3ed70a41b3.js +0 -1
  133. recce/data/_next/static/chunks/8d700b6a.7fe2c8c3f4e333a6.js +0 -1
  134. recce/data/_next/static/chunks/a69d64b4-d6890125a87b0aba.js +0 -1
  135. recce/data/_next/static/chunks/ae307f12-01100009689ace61.js +0 -1
  136. recce/data/_next/static/chunks/app/_not-found/page-c7ef8ed6dc07aaeb.js +0 -1
  137. recce/data/_next/static/chunks/app/layout-744f0a78e9e50e60.js +0 -1
  138. recce/data/_next/static/chunks/app/page-e8f798c2ae3f59c2.js +0 -1
  139. recce/data/_next/static/chunks/c0015c5c-82c219792582c104.js +0 -1
  140. recce/data/_next/static/chunks/d90cfbaa-e7d779b3912afeec.js +0 -1
  141. recce/data/_next/static/chunks/e07c302e-cd170429646873e1.js +0 -1
  142. recce/data/_next/static/chunks/fa5fb511-15fb438349ad5b97.js +0 -1
  143. recce/data/_next/static/chunks/framework-7950757d31580329.js +0 -1
  144. recce/data/_next/static/chunks/main-app-4df79eb11c34d43c.js +0 -1
  145. recce/data/_next/static/chunks/main-cd6c104af638214a.js +0 -1
  146. recce/data/_next/static/chunks/pages/_app-73008661edbd5e05.js +0 -1
  147. recce/data/_next/static/chunks/pages/_error-cf8bbdc3cf76c83f.js +0 -1
  148. recce/data/_next/static/chunks/webpack-84df6dd5ae3cf908.js +0 -1
  149. recce/data/_next/static/css/188a3a1687e2a064.css +0 -1
  150. recce/data/_next/static/css/8edca58d4abcf908.css +0 -14
  151. recce/data/_next/static/css/abdb9814a3dd18bb.css +0 -1
  152. recce/data/_next/static/css/c21263c1520b615b.css +0 -1
  153. recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
  154. recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
  155. recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
  156. recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
  157. recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
  158. recce/state.py +0 -865
  159. recce_nightly-1.15.0.20250806.dist-info/RECORD +0 -156
  160. tests/test_state.py +0 -134
  161. /recce/data/_next/static/{Q_5ThPsmamd4VAGXuqwgi → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
  162. /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
  163. /recce/data/_next/static/media/{montserrat-cyrillic-ext-800-normal.e6e0d8d0.woff → montserrat-cyrillic-ext-800-normal.a4fa76b5.woff} +0 -0
  164. /recce/data/_next/static/media/{reload-image.79aabb7d.svg → reload-image.7aa931c7.svg} +0 -0
  165. {recce_nightly-1.15.0.20250806.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +0 -0
  166. {recce_nightly-1.15.0.20250806.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
  167. {recce_nightly-1.15.0.20250806.dist-info → recce_nightly-1.26.0.20251124.dist-info}/licenses/LICENSE +0 -0
recce/models/types.py CHANGED
@@ -5,6 +5,8 @@ from typing import Dict, List, Literal, Optional, Set
5
5
 
6
6
  from pydantic import UUID4, BaseModel, Field
7
7
 
8
+ from recce.util.pydantic_model import pydantic_model_dump
9
+
8
10
 
9
11
  class RunType(Enum):
10
12
  SIMPLE = "simple"
@@ -36,8 +38,6 @@ class RunStatus(Enum):
36
38
  FAILED = "failed"
37
39
  CANCELLED = "cancelled"
38
40
  RUNNING = "running"
39
- # This is a special status only in v0.36.0. Replaced by FINISHED. To be removed in the future.
40
- SUCCESSFUL = "successful"
41
41
 
42
42
 
43
43
  class Run(BaseModel):
@@ -52,6 +52,39 @@ class Run(BaseModel):
52
52
  run_id: UUID4 = Field(default_factory=uuid.uuid4)
53
53
  run_at: str = Field(default_factory=lambda: datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"))
54
54
 
55
+ def __init__(self, **data):
56
+ type = data.get("type")
57
+
58
+ if "result" in data and data["result"] is not None:
59
+ result = data.get("result")
60
+
61
+ if type in [RunType.QUERY.value, RunType.QUERY_BASE.value]:
62
+ from recce.tasks.query import QueryResult
63
+
64
+ data["result"] = pydantic_model_dump(QueryResult(**result))
65
+ elif type == RunType.QUERY_DIFF.value:
66
+ from recce.tasks.query import QueryDiffResult
67
+
68
+ data["result"] = pydantic_model_dump(QueryDiffResult(**result))
69
+ elif type == RunType.PROFILE.value:
70
+ from recce.tasks.profile import ProfileResult
71
+
72
+ data["result"] = pydantic_model_dump(ProfileResult(**result))
73
+ elif type == RunType.PROFILE_DIFF.value:
74
+ from recce.tasks.profile import ProfileDiffResult
75
+
76
+ data["result"] = pydantic_model_dump(ProfileDiffResult(**result))
77
+ elif type == RunType.VALUE_DIFF.value:
78
+ from recce.tasks.valuediff import ValueDiffResult
79
+
80
+ data["result"] = pydantic_model_dump(ValueDiffResult(**result))
81
+ elif type == RunType.VALUE_DIFF_DETAIL.value:
82
+ from recce.tasks.valuediff import ValueDiffDetailResult
83
+
84
+ data["result"] = pydantic_model_dump(ValueDiffDetailResult(**result))
85
+
86
+ super().__init__(**data)
87
+
55
88
 
56
89
  class Check(BaseModel):
57
90
  name: str
recce/pull_request.py CHANGED
@@ -83,7 +83,7 @@ def fetch_pr_metadata_from_event_path() -> Optional[dict]:
83
83
  github_repository = os.getenv("GITHUB_REPOSITORY")
84
84
  if event_path:
85
85
  try:
86
- with open(event_path, "r") as event_file:
86
+ with open(event_path, "r", encoding="utf-8") as event_file:
87
87
  event_data = json.load(event_file)
88
88
 
89
89
  pr_id = event_data["number"]
recce/run.py CHANGED
@@ -301,7 +301,7 @@ def process_failed_checks(failed_checks: List[dict], error_log=None):
301
301
  content += markdown_table(failed_check_table).set_params(quote=False, row_sep="markdown").get_markdown()
302
302
 
303
303
  if error_log:
304
- with open(error_log, "w") as f:
304
+ with open(error_log, "w", encoding="utf-8") as f:
305
305
  f.write(content)
306
306
  print(f"The failed checks are stored at '{error_log}'")
307
307
  else:
@@ -370,7 +370,7 @@ async def cli_run(output_state_file: str, **kwargs):
370
370
  dirs = os.path.dirname(summary_path)
371
371
  if dirs:
372
372
  os.makedirs(dirs, exist_ok=True)
373
- with open(summary_path, "w") as f:
373
+ with open(summary_path, "w", encoding="utf-8") as f:
374
374
  f.write(generate_markdown_summary(ctx))
375
375
  console.print(f"The summary is stored at '{summary_path}'")
376
376
 
recce/server.py CHANGED
@@ -30,7 +30,7 @@ from starlette.middleware.gzip import GZipMiddleware
30
30
  from starlette.middleware.sessions import SessionMiddleware
31
31
  from starlette.websockets import WebSocketDisconnect
32
32
 
33
- from . import __latest_version__, __version__, event
33
+ from . import __latest_version__, __version__, event, is_recce_cloud_instance
34
34
  from .apis.check_api import check_router
35
35
  from .apis.run_api import run_router
36
36
  from .config import RecceConfig
@@ -44,12 +44,17 @@ from .connect_to_cloud import (
44
44
  from .core import RecceContext, default_context, load_context
45
45
  from .event import get_recce_api_token, log_api_event, log_single_env_event
46
46
  from .exceptions import RecceException
47
+ from .github import is_github_codespace
47
48
  from .models.types import CllData
48
49
  from .run import load_preset_checks
49
50
  from .state import RecceShareStateManager, RecceStateLoader
50
51
 
51
52
  logger = logging.getLogger("uvicorn")
52
53
 
54
+ # Idle timeout check interval bounds (in seconds)
55
+ MAX_CHECK_INTERVAL = 30
56
+ MIN_CHECK_INTERVAL = 1
57
+
53
58
 
54
59
  class RecceServerMode(str, Enum):
55
60
  server = "server"
@@ -73,7 +78,11 @@ class AppState:
73
78
  auth_options: Optional[dict] = None
74
79
  lifetime: Optional[int] = None
75
80
  lifetime_expired_at: Optional[datetime] = None
81
+ idle_timeout: Optional[int] = None
82
+ last_activity: Optional[dict] = None
76
83
  share_url: Optional[str] = None
84
+ organization_name: Optional[str] = None
85
+ web_url: Optional[str] = None
77
86
  host: Optional[str] = None
78
87
  port: Optional[int] = None
79
88
 
@@ -81,7 +90,7 @@ class AppState:
81
90
  def schedule_lifetime_termination(app_state):
82
91
  def terminating_server():
83
92
  pid = os.getpid()
84
- logger.info(f"Terminating server process [{pid}] manually")
93
+ logger.info(f"Terminating server process [{pid}] manually due to lifetime expiration")
85
94
  os.kill(pid, signal.SIGINT)
86
95
 
87
96
  # Terminate the server process after the specified lifetime
@@ -90,6 +99,56 @@ def schedule_lifetime_termination(app_state):
90
99
  asyncio.get_running_loop().call_later(app_state.lifetime, terminating_server)
91
100
 
92
101
 
102
+ def schedule_idle_timeout_check(app_state):
103
+ """
104
+ Schedule periodic checks for idle timeout.
105
+ If the server has been idle for longer than idle_timeout, terminate it.
106
+ """
107
+ # Track last activity time in app_state
108
+ app_state.last_activity = {"time": datetime.now(utc)}
109
+
110
+ def terminating_server_idle():
111
+ pid = os.getpid()
112
+ logger.info(f"Terminating server process [{pid}] manually due to idle timeout")
113
+ os.kill(pid, signal.SIGINT)
114
+
115
+ async def check_idle_timeout():
116
+ """Periodically check if the server has been idle for too long"""
117
+ # Use smaller check interval if idle_timeout is very short
118
+ # Check at least every MAX_CHECK_INTERVAL seconds, but also check when idle_timeout is approaching
119
+ check_interval = min(MAX_CHECK_INTERVAL, max(MIN_CHECK_INTERVAL, app_state.idle_timeout // 3))
120
+
121
+ logger.debug(f"[Idle Timeout] Starting idle timeout checker with {check_interval}s check interval")
122
+
123
+ while True:
124
+ await asyncio.sleep(check_interval)
125
+
126
+ idle_seconds = (datetime.now(utc) - app_state.last_activity["time"]).total_seconds()
127
+ remaining_seconds = app_state.idle_timeout - idle_seconds
128
+
129
+ # Always log the countdown for debugging
130
+ if remaining_seconds > 0:
131
+ logger.debug(
132
+ f"[Idle Timeout] Server idle for {idle_seconds:.1f}s / {app_state.idle_timeout}s "
133
+ f"(remaining: {remaining_seconds:.1f}s)"
134
+ )
135
+
136
+ if idle_seconds >= app_state.idle_timeout:
137
+ logger.info(
138
+ f"[Idle Timeout] Threshold reached! Server has been idle for {idle_seconds:.0f} seconds "
139
+ f"(threshold: {app_state.idle_timeout} seconds)"
140
+ )
141
+ terminating_server_idle()
142
+ break
143
+
144
+ # Start the idle timeout check task
145
+ logger.info(f"[Configuration] The idle timeout of the server is {app_state.idle_timeout} seconds")
146
+
147
+ # Create task using asyncio.create_task which works in async context
148
+ task = asyncio.create_task(check_idle_timeout())
149
+ logger.debug(f"[Idle Timeout] Background task created: {task}")
150
+
151
+
93
152
  def setup_server(app_state: AppState) -> RecceContext:
94
153
  from rich.console import Console
95
154
 
@@ -160,6 +219,12 @@ async def lifespan(fastapi: FastAPI):
160
219
  ctx = None
161
220
  app_state: AppState = app.state
162
221
 
222
+ # Ensure logger is at DEBUG level if debug mode is enabled
223
+ # This is needed because uvicorn might reset logger configuration
224
+ if hasattr(app_state, "kwargs") and app_state.kwargs.get("debug"):
225
+ logger.setLevel(logging.DEBUG)
226
+ logger.debug("Debug mode enabled - logger set to DEBUG level")
227
+
163
228
  if app_state.command == "server":
164
229
  ctx = setup_server(app_state)
165
230
  elif app_state.command == "read-only":
@@ -170,6 +235,11 @@ async def lifespan(fastapi: FastAPI):
170
235
  if app_state.lifetime is not None and app_state.lifetime > 0:
171
236
  schedule_lifetime_termination(app_state)
172
237
 
238
+ # Schedule idle timeout check if idle_timeout is set
239
+ if app_state.idle_timeout is not None and app_state.idle_timeout > 0:
240
+ logger.debug(f"[Idle Timeout] Scheduling idle timeout check with {app_state.idle_timeout} seconds")
241
+ schedule_idle_timeout_check(app_state)
242
+
173
243
  yield
174
244
 
175
245
  if app_state.command == "server":
@@ -185,7 +255,7 @@ app = FastAPI(lifespan=lifespan)
185
255
 
186
256
  def verify_json_file(file_path: str) -> bool:
187
257
  try:
188
- with open(file_path, "r") as f:
258
+ with open(file_path, "r", encoding="utf-8") as f:
189
259
  json.load(f)
190
260
  except Exception:
191
261
  return False
@@ -242,6 +312,27 @@ app.add_middleware(
242
312
  )
243
313
 
244
314
 
315
+ @app.middleware("http")
316
+ async def track_activity_for_idle_timeout(request: Request, call_next):
317
+ """Track activity time for idle timeout check"""
318
+ # Exclude paths that should not reset idle timer
319
+ # Health checks and monitoring endpoints don't count as user activity
320
+ excluded_paths = ["/api/health", "/api/ws"]
321
+
322
+ # Update last activity time BEFORE processing request if idle timeout is enabled
323
+ # This ensures long-running requests don't get terminated mid-execution
324
+ app_state: AppState = app.state
325
+ if app_state.last_activity is not None:
326
+ if request.url.path not in excluded_paths:
327
+ app_state.last_activity["time"] = datetime.now(utc)
328
+ logger.debug(f"[Idle Timeout] ✓ Activity detected: {request.method} {request.url.path} - Timer reset")
329
+ else:
330
+ logger.debug(f"[Idle Timeout] Excluded path (no timer reset): {request.method} {request.url.path}")
331
+
332
+ response = await call_next(request)
333
+ return response
334
+
335
+
245
336
  @app.middleware("http")
246
337
  async def set_context_by_cookie(request: Request, call_next):
247
338
  response = await call_next(request)
@@ -280,8 +371,12 @@ class RecceInstanceInfoOut(BaseModel):
280
371
  preview: bool
281
372
  single_env: bool
282
373
  authed: bool
374
+ cloud_instance: bool
283
375
  lifetime_expired_at: Optional[datetime] = None
284
376
  share_url: Optional[str] = None
377
+ session_id: Optional[str] = None
378
+ organization_name: Optional[str] = None
379
+ web_url: Optional[str] = None
285
380
 
286
381
 
287
382
  @app.get("/api/instance-info", response_model=RecceInstanceInfoOut, response_model_exclude_none=True)
@@ -299,8 +394,12 @@ async def recce_instance_info():
299
394
  "preview": flag.get("preview", False),
300
395
  "single_env": single_env,
301
396
  "authed": True if api_token else False,
397
+ "cloud_instance": is_recce_cloud_instance(),
302
398
  "lifetime_expired_at": app_state.lifetime_expired_at, # UTC timezone
303
399
  "share_url": app_state.share_url,
400
+ "session_id": app_state.state_loader.session_id if app_state.state_loader else None,
401
+ "organization_name": app_state.organization_name,
402
+ "web_url": app_state.web_url,
304
403
  # TODO: Add more instance info which won't change during the instance lifecycle
305
404
  # review_mode
306
405
  # cloud_mode
@@ -328,6 +427,7 @@ async def get_info():
328
427
  """
329
428
  context = default_context()
330
429
  demo = os.environ.get("DEMO", False)
430
+ is_codespace = is_github_codespace()
331
431
 
332
432
  if demo:
333
433
  state = context.export_demo_state()
@@ -352,6 +452,7 @@ async def get_info():
352
452
  "pull_request": state.pull_request.to_dict() if state.pull_request else None,
353
453
  "lineage": lineage_diff,
354
454
  "demo": bool(demo),
455
+ "codespace": bool(is_codespace),
355
456
  "cloud_mode": context.state_loader.cloud_mode,
356
457
  "file_mode": context.state_loader.state_file is not None,
357
458
  "filename": filename,
@@ -727,6 +828,7 @@ async def get_user_info():
727
828
  except Exception as e:
728
829
  raise HTTPException(status_code=400, detail=str(e))
729
830
 
831
+
730
832
  api_prefix = "/api"
731
833
  app.include_router(check_router, prefix=api_prefix)
732
834
  app.include_router(run_router, prefix=api_prefix)
@@ -0,0 +1,31 @@
1
+ from .cloud import (
2
+ CloudStateLoader,
3
+ RecceCloudStateManager,
4
+ RecceShareStateManager,
5
+ s3_sse_c_headers,
6
+ )
7
+ from .const import ErrorMessage
8
+ from .local import FileStateLoader
9
+ from .state import (
10
+ ArtifactsRoot,
11
+ GitRepoInfo,
12
+ PullRequestInfo,
13
+ RecceState,
14
+ RecceStateMetadata,
15
+ )
16
+ from .state_loader import RecceStateLoader
17
+
18
+ __all__ = [
19
+ "ArtifactsRoot",
20
+ "ErrorMessage",
21
+ "RecceCloudStateManager",
22
+ "RecceShareStateManager",
23
+ "RecceState",
24
+ "RecceStateLoader",
25
+ "CloudStateLoader",
26
+ "FileStateLoader",
27
+ "RecceStateMetadata",
28
+ "s3_sse_c_headers",
29
+ "GitRepoInfo",
30
+ "PullRequestInfo",
31
+ ]