Skip to content

Commit 67483b2

Browse files
committed
chore: removes falcon dependency
1 parent d221cf8 commit 67483b2

3 files changed

Lines changed: 89 additions & 27 deletions

File tree

git_analytics/collectors/code_churn.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,9 @@ def __init__(
1616
self._churn_days = churn_days
1717
self._churn_window_seconds = churn_days * 24 * 60 * 60
1818

19-
# Начало периода отчетности устанавливается динамически:
20-
# первый_коммит + churn_days
2119
self._report_start: Optional[datetime] = None
2220
self._first_commit_date: Optional[datetime] = None
2321

24-
# file_path -> normalized_line -> deque[birth_ts]
2522
self._live_lines: Dict[str, Dict[str, deque[int]]] = defaultdict(lambda: defaultdict(deque))
2623

2724
self._added_lines_in_period = 0
@@ -38,7 +35,6 @@ def consume(self, ctx: CommitContext) -> None:
3835
commit_dt = ctx.committed_datetime.astimezone(timezone.utc)
3936
commit_ts = int(commit_dt.timestamp())
4037

41-
# Устанавливаем период отчетности при первом коммите
4238
if self._first_commit_date is None:
4339
self._first_commit_date = commit_dt
4440
self._report_start = self._first_commit_date + timedelta(days=self._churn_days)

git_analytics/web_app.py

Lines changed: 89 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,101 @@
11
import os
2-
from typing import Dict
2+
import json
3+
from typing import Dict, Callable
34
from dataclasses import is_dataclass, asdict
45
from datetime import date, datetime
56

6-
from falcon import App as FalconApp
77

8+
def create_web_app(data: Dict[str, object]) -> Callable:
9+
static_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
810

9-
def create_web_app(data: Dict[str, object]) -> FalconApp:
10-
app = FalconApp()
11+
def _to_json_serializable(obj):
12+
"""Recursively convert dataclasses and other objects to JSON-serializable dict."""
13+
if isinstance(obj, (date, datetime)):
14+
return obj.isoformat()
15+
elif is_dataclass(obj) and not isinstance(obj, type):
16+
return _to_json_serializable(asdict(obj))
17+
elif isinstance(obj, dict):
18+
return {key: _to_json_serializable(value) for key, value in obj.items()}
19+
elif isinstance(obj, (list, tuple)):
20+
return [_to_json_serializable(item) for item in obj]
21+
else:
22+
return obj
1123

12-
class StatisticsResource:
13-
def _to_json_serializable(self, obj):
14-
"""Recursively convert dataclasses and other objects to JSON-serializable dict."""
15-
if isinstance(obj, (date, datetime)):
16-
return obj.isoformat()
17-
elif is_dataclass(obj) and not isinstance(obj, type):
18-
return self._to_json_serializable(asdict(obj))
19-
elif isinstance(obj, dict):
20-
return {key: self._to_json_serializable(value) for key, value in obj.items()}
21-
elif isinstance(obj, (list, tuple)):
22-
return [self._to_json_serializable(item) for item in obj]
24+
def _serve_static_file(environ, start_response, file_path):
25+
try:
26+
with open(file_path, "rb") as f:
27+
content = f.read()
28+
29+
if file_path.endswith(".html"):
30+
content_type = "text/html; charset=utf-8"
31+
elif file_path.endswith(".js"):
32+
content_type = "application/javascript; charset=utf-8"
33+
elif file_path.endswith(".css"):
34+
content_type = "text/css; charset=utf-8"
35+
elif file_path.endswith(".json"):
36+
content_type = "application/json; charset=utf-8"
37+
elif file_path.endswith(".png"):
38+
content_type = "image/png"
39+
elif file_path.endswith(".jpg") or file_path.endswith(".jpeg"):
40+
content_type = "image/jpeg"
41+
elif file_path.endswith(".svg"):
42+
content_type = "image/svg+xml"
43+
elif file_path.endswith(".ico"):
44+
content_type = "image/x-icon"
2345
else:
24-
return obj
46+
content_type = "application/octet-stream"
47+
48+
start_response("200 OK", [("Content-Type", content_type), ("Content-Length", str(len(content)))])
49+
return [content]
50+
except FileNotFoundError:
51+
start_response("404 Not Found", [("Content-Type", "text/plain")])
52+
return [b"404 - File Not Found"]
53+
except Exception as e:
54+
start_response("500 Internal Server Error", [("Content-Type", "text/plain")])
55+
return [f"500 - Internal Server Error: {str(e)}".encode("utf-8")]
56+
57+
def wsgi_app(environ, start_response):
58+
"""WSGI application."""
59+
path = environ.get("PATH_INFO", "/")
60+
method = environ.get("REQUEST_METHOD", "GET")
61+
62+
# API endpoint
63+
if path == "/api/statistics" and method == "GET":
64+
try:
65+
serializable_data = _to_json_serializable(data)
66+
json_data = json.dumps(serializable_data, ensure_ascii=False, indent=2)
67+
response_body = json_data.encode("utf-8")
68+
69+
start_response(
70+
"200 OK",
71+
[("Content-Type", "application/json; charset=utf-8"), ("Content-Length", str(len(response_body)))],
72+
)
73+
return [response_body]
74+
except Exception as e:
75+
error_body = json.dumps({"error": str(e)}).encode("utf-8")
76+
start_response(
77+
"500 Internal Server Error",
78+
[("Content-Type", "application/json; charset=utf-8"), ("Content-Length", str(len(error_body)))],
79+
)
80+
return [error_body]
81+
82+
# static files
83+
if path == "/":
84+
path = "/index.html"
85+
86+
# Security: prevent path traversal
87+
path = path.lstrip("/")
88+
if ".." in path or path.startswith("/"):
89+
start_response("403 Forbidden", [("Content-Type", "text/plain")])
90+
return [b"403 - Forbidden"]
2591

26-
def on_get(self, req, resp):
27-
resp.media = self._to_json_serializable(data)
92+
file_path = os.path.join(static_path, path)
2893

29-
app.add_route("/api/statistics", StatisticsResource())
94+
# Security: ensure the file is within the static directory
95+
if not os.path.abspath(file_path).startswith(os.path.abspath(static_path)):
96+
start_response("403 Forbidden", [("Content-Type", "text/plain")])
97+
return [b"403 - Forbidden"]
3098

31-
static_path = os.path.dirname(os.path.abspath(__file__)) + "/static/"
32-
app.add_static_route("/", static_path)
99+
return _serve_static_file(environ, start_response, file_path)
33100

34-
return app
101+
return wsgi_app

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ packages = [{include = "git_analytics"}]
1313
[tool.poetry.dependencies]
1414
python = ">=3.7"
1515
GitPython = "~=3.1.0"
16-
falcon = "~=3.1.0"
1716

1817
[tool.poetry.group.dev.dependencies]
1918
pytest = "^7.4"

0 commit comments

Comments
 (0)