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.
- recce/VERSION +1 -1
- recce/__init__.py +5 -0
- recce/adapter/dbt_adapter/__init__.py +12 -3
- recce/artifact.py +74 -1
- recce/cli.py +642 -101
- recce/config.py +2 -2
- recce/connect_to_cloud.py +1 -1
- recce/core.py +2 -2
- recce/data/404.html +1 -1
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._head.txt +8 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +5 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
- recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
- recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
- recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
- recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
- recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
- recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
- recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
- recce/data/_next/static/chunks/99d638224186c118.js +1 -0
- recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
- recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
- recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
- recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.js +3 -0
- recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-cyrillic-800-normal.bd5c9f50.woff → montserrat-cyrillic-800-normal.f9d58125.woff} +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-latin-800-normal.fc315020.woff → montserrat-latin-800-normal.d5761935.woff} +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
- recce/data/_next/static/media/{montserrat-latin-ext-800-normal.2e5381b2.woff → montserrat-latin-ext-800-normal.b671449b.woff} +0 -0
- recce/data/_next/static/media/{montserrat-vietnamese-800-normal.20c545e6.woff → montserrat-vietnamese-800-normal.9f7b8541.woff} +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_not-found/__next._full.txt +17 -0
- recce/data/_not-found/__next._head.txt +8 -0
- recce/data/_not-found/__next._index.txt +8 -0
- recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
- recce/data/_not-found/__next._not-found.txt +4 -0
- recce/data/_not-found/__next._tree.txt +3 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/index.html +1 -1
- recce/data/index.txt +21 -23
- recce/event/__init__.py +9 -8
- recce/event/collector.py +3 -1
- recce/event/track.py +10 -0
- recce/github.py +1 -1
- recce/mcp_server.py +716 -0
- recce/models/types.py +35 -2
- recce/pull_request.py +1 -1
- recce/run.py +2 -2
- recce/server.py +105 -3
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +632 -0
- recce/state/const.py +26 -0
- recce/state/local.py +56 -0
- recce/state/state.py +119 -0
- recce/state/state_loader.py +174 -0
- recce/summary.py +21 -1
- recce/tasks/dataframe.py +63 -1
- recce/tasks/rowcount.py +4 -1
- recce/tasks/schema.py +4 -1
- recce/util/api_token.py +9 -2
- recce/util/breaking.py +1 -1
- recce/util/io.py +2 -2
- recce/util/lineage.py +14 -18
- recce/util/recce_cloud.py +187 -7
- recce/yaml/__init__.py +2 -2
- recce_cloud/__init__.py +24 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +111 -0
- recce_cloud/api/client.py +150 -0
- recce_cloud/api/exceptions.py +26 -0
- recce_cloud/api/factory.py +63 -0
- recce_cloud/api/github.py +76 -0
- recce_cloud/api/gitlab.py +82 -0
- recce_cloud/artifact.py +57 -0
- recce_cloud/ci_providers/__init__.py +9 -0
- recce_cloud/ci_providers/base.py +82 -0
- recce_cloud/ci_providers/detector.py +147 -0
- recce_cloud/ci_providers/github_actions.py +136 -0
- recce_cloud/ci_providers/gitlab_ci.py +130 -0
- recce_cloud/cli.py +245 -0
- recce_cloud/upload.py +214 -0
- {recce_nightly-1.15.0.20250806.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +54 -28
- recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
- {recce_nightly-1.15.0.20250806.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/test_dbt_cll.py +4 -2
- tests/recce_cloud/__init__.py +0 -0
- tests/recce_cloud/test_ci_providers.py +351 -0
- tests/recce_cloud/test_cli.py +372 -0
- tests/recce_cloud/test_client.py +273 -0
- tests/recce_cloud/test_platform_clients.py +333 -0
- tests/test_cli.py +106 -3
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_core.py +147 -0
- tests/test_mcp_server.py +332 -0
- tests/test_server.py +6 -6
- tests/test_summary.py +14 -6
- recce/data/_next/static/Q_5ThPsmamd4VAGXuqwgi/_buildManifest.js +0 -1
- recce/data/_next/static/chunks/0376eeba-3db2196398d62270.js +0 -1
- recce/data/_next/static/chunks/068b80ea-833a129468ee1622.js +0 -1
- recce/data/_next/static/chunks/0ddaf06c-c7961285f66460f6.js +0 -1
- recce/data/_next/static/chunks/1268aea1-6dc1251c01bd724b.js +0 -54
- recce/data/_next/static/chunks/12f8fac4-16838e42d28d45c3.js +0 -1
- recce/data/_next/static/chunks/235b8375-8c84c51d7bd4f6aa.js +0 -1
- recce/data/_next/static/chunks/2541941f-2cd3a7c2d629bd33.js +0 -1
- recce/data/_next/static/chunks/273-f3fa401bd2b6fc91.js +0 -10
- recce/data/_next/static/chunks/2fc37c1e-910deebeb3d77c90.js +0 -1
- recce/data/_next/static/chunks/338-2e7eed5135c64550.js +0 -30
- recce/data/_next/static/chunks/367-ab8b16dd5f8586ca.js +0 -1
- recce/data/_next/static/chunks/3a92ee20-0400ffe460c7c803.js +0 -1
- recce/data/_next/static/chunks/62446465-423c03bb8c1f59b6.js +0 -1
- recce/data/_next/static/chunks/6af7f9e9-60aa8706f49dae45.js +0 -1
- recce/data/_next/static/chunks/6cf54382-49d52ae6e564e2ac.js +0 -1
- recce/data/_next/static/chunks/6dc81886-78e2efe4538794ae.js +0 -1
- recce/data/_next/static/chunks/715e4acc-9e2e6df4eb3809d1.js +0 -1
- recce/data/_next/static/chunks/72-181b430654230f0e.js +0 -1
- recce/data/_next/static/chunks/786-774e3e3ed70a41b3.js +0 -1
- recce/data/_next/static/chunks/8d700b6a.7fe2c8c3f4e333a6.js +0 -1
- recce/data/_next/static/chunks/a69d64b4-d6890125a87b0aba.js +0 -1
- recce/data/_next/static/chunks/ae307f12-01100009689ace61.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-c7ef8ed6dc07aaeb.js +0 -1
- recce/data/_next/static/chunks/app/layout-744f0a78e9e50e60.js +0 -1
- recce/data/_next/static/chunks/app/page-e8f798c2ae3f59c2.js +0 -1
- recce/data/_next/static/chunks/c0015c5c-82c219792582c104.js +0 -1
- recce/data/_next/static/chunks/d90cfbaa-e7d779b3912afeec.js +0 -1
- recce/data/_next/static/chunks/e07c302e-cd170429646873e1.js +0 -1
- recce/data/_next/static/chunks/fa5fb511-15fb438349ad5b97.js +0 -1
- recce/data/_next/static/chunks/framework-7950757d31580329.js +0 -1
- recce/data/_next/static/chunks/main-app-4df79eb11c34d43c.js +0 -1
- recce/data/_next/static/chunks/main-cd6c104af638214a.js +0 -1
- recce/data/_next/static/chunks/pages/_app-73008661edbd5e05.js +0 -1
- recce/data/_next/static/chunks/pages/_error-cf8bbdc3cf76c83f.js +0 -1
- recce/data/_next/static/chunks/webpack-84df6dd5ae3cf908.js +0 -1
- recce/data/_next/static/css/188a3a1687e2a064.css +0 -1
- recce/data/_next/static/css/8edca58d4abcf908.css +0 -14
- recce/data/_next/static/css/abdb9814a3dd18bb.css +0 -1
- recce/data/_next/static/css/c21263c1520b615b.css +0 -1
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/state.py +0 -865
- recce_nightly-1.15.0.20250806.dist-info/RECORD +0 -156
- tests/test_state.py +0 -134
- /recce/data/_next/static/{Q_5ThPsmamd4VAGXuqwgi → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- /recce/data/_next/static/media/{montserrat-cyrillic-ext-800-normal.e6e0d8d0.woff → montserrat-cyrillic-ext-800-normal.a4fa76b5.woff} +0 -0
- /recce/data/_next/static/media/{reload-image.79aabb7d.svg → reload-image.7aa931c7.svg} +0 -0
- {recce_nightly-1.15.0.20250806.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +0 -0
- {recce_nightly-1.15.0.20250806.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
- {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)
|
recce/state/__init__.py
ADDED
|
@@ -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
|
+
]
|