Skip to content

Commit 26299a3

Browse files
committed
chore: second group of duplications removed
1 parent 315b8e5 commit 26299a3

5 files changed

Lines changed: 53 additions & 78 deletions

File tree

backend/app/db/docs/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
from app.db.docs.saved_script import SavedScriptDocument
2222
from app.db.docs.user import UserDocument
2323
from app.db.docs.user_settings import (
24-
EditorSettings,
25-
NotificationSettings,
2624
UserSettingsDocument,
2725
UserSettingsSnapshotDocument,
2826
)
@@ -60,8 +58,6 @@
6058
# User Settings
6159
"UserSettingsDocument",
6260
"UserSettingsSnapshotDocument",
63-
"NotificationSettings",
64-
"EditorSettings",
6561
# Saga
6662
"SagaDocument",
6763
# DLQ

backend/app/db/docs/user_settings.py

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22
from typing import Any
33

44
from beanie import Document, Indexed
5-
from pydantic import BaseModel, ConfigDict, Field, field_validator
5+
from pydantic import BaseModel, ConfigDict, Field
66

77
from app.domain.enums import NotificationChannel, Theme
88

99

1010
class NotificationSettings(BaseModel):
11-
"""User notification preferences (embedded document).
12-
13-
Copied from user_settings.py NotificationSettings.
14-
"""
11+
"""User notification preferences (embedded document)."""
1512

1613
model_config = ConfigDict(from_attributes=True)
1714

@@ -23,10 +20,7 @@ class NotificationSettings(BaseModel):
2320

2421

2522
class EditorSettings(BaseModel):
26-
"""Code editor preferences (embedded document).
27-
28-
Copied from user_settings.py EditorSettings.
29-
"""
23+
"""Code editor preferences (embedded document)."""
3024

3125
model_config = ConfigDict(from_attributes=True)
3226

@@ -36,26 +30,9 @@ class EditorSettings(BaseModel):
3630
word_wrap: bool = True
3731
show_line_numbers: bool = True
3832

39-
@field_validator("font_size")
40-
@classmethod
41-
def validate_font_size(cls, v: int) -> int:
42-
if v < 8 or v > 32:
43-
raise ValueError("Font size must be between 8 and 32")
44-
return v
45-
46-
@field_validator("tab_size")
47-
@classmethod
48-
def validate_tab_size(cls, v: int) -> int:
49-
if v not in (2, 4, 8):
50-
raise ValueError("Tab size must be 2, 4, or 8")
51-
return v
52-
5333

5434
class UserSettingsDocument(Document):
55-
"""Complete user settings model.
56-
57-
Copied from UserSettings schema.
58-
"""
35+
"""Complete user settings model."""
5936

6037
user_id: Indexed(str, unique=True) # type: ignore[valid-type]
6138
theme: Theme = Theme.AUTO

backend/app/schemas_pydantic/user_settings.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime, timezone
22
from typing import Any
33

4-
from pydantic import BaseModel, ConfigDict, Field, field_validator
4+
from pydantic import BaseModel, ConfigDict, Field
55

66
from app.domain.enums import EventType, NotificationChannel, Theme
77

@@ -29,20 +29,6 @@ class EditorSettings(BaseModel):
2929
word_wrap: bool = True
3030
show_line_numbers: bool = True
3131

32-
@field_validator("font_size")
33-
@classmethod
34-
def validate_font_size(cls, v: int) -> int:
35-
if v < 8 or v > 32:
36-
raise ValueError("Font size must be between 8 and 32")
37-
return v
38-
39-
@field_validator("tab_size")
40-
@classmethod
41-
def validate_tab_size(cls, v: int) -> int:
42-
if v not in (2, 4, 8):
43-
raise ValueError("Tab size must be 2, 4, or 8")
44-
return v
45-
4632

4733
class UserSettings(BaseModel):
4834
"""Complete user settings model"""

backend/app/services/user_settings_service.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from app.db import UserSettingsRepository
99
from app.domain.enums import EventType, Theme
1010
from app.domain.events import EventMetadata, UserSettingsUpdatedEvent
11+
from app.domain.exceptions import ValidationError
1112
from app.domain.user import (
1213
DomainEditorSettings,
1314
DomainNotificationSettings,
@@ -86,6 +87,9 @@ async def update_user_settings(
8687
self, user_id: str, updates: DomainUserSettingsUpdate, reason: str | None = None
8788
) -> DomainUserSettings:
8889
"""Upsert provided fields into current settings, publish minimal event, and cache."""
90+
if updates.editor is not None:
91+
self._validate_editor_settings(updates.editor)
92+
8993
current = await self.get_user_settings(user_id)
9094

9195
changes = {k: v for k, v in dataclasses.asdict(updates).items() if v is not None}
@@ -218,6 +222,13 @@ def _build_settings(data: dict[str, Any]) -> DomainUserSettings:
218222
filtered["editor"] = DomainEditorSettings(**filtered["editor"])
219223
return DomainUserSettings(**filtered)
220224

225+
@staticmethod
226+
def _validate_editor_settings(editor: DomainEditorSettings) -> None:
227+
if not 8 <= editor.font_size <= 32:
228+
raise ValidationError("Font size must be between 8 and 32")
229+
if editor.tab_size not in (2, 4, 8):
230+
raise ValidationError("Tab size must be 2, 4, or 8")
231+
221232
async def invalidate_cache(self, user_id: str) -> None:
222233
"""Invalidate cached settings for a user."""
223234
if self._cache.pop(user_id, None) is not None:

backend/tests/load/plot_report.py

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
import argparse
44
import json
55
from pathlib import Path
6-
from typing import Dict, List, Tuple, TypedDict
6+
from typing import TYPE_CHECKING, Callable, Dict, List, Tuple, TypedDict
77

88
import matplotlib.pyplot as plt
99

10+
if TYPE_CHECKING:
11+
from matplotlib.axes import Axes
12+
1013

1114
class LatencyStats(TypedDict, total=False):
1215
p50: int
@@ -75,6 +78,28 @@ def _top_endpoints(report: ReportDict, top_n: int = 10) -> List[Tuple[str, Endpo
7578
return items[:top_n]
7679

7780

81+
def _save_bar_chart(
82+
labels: List[str],
83+
title: str,
84+
ylabel: str,
85+
out_path: Path,
86+
plot_bars: Callable[[Axes, range], None],
87+
) -> Path:
88+
x = range(len(labels))
89+
fig, ax = plt.subplots(figsize=(max(10, len(labels) * 0.6), 5))
90+
plot_bars(ax, x)
91+
ax.set_ylabel(ylabel)
92+
ax.set_title(title)
93+
ax.set_xticks(list(x))
94+
ax.set_xticklabels(labels, rotation=45, ha="right")
95+
ax.grid(True, axis="y", alpha=0.3)
96+
ax.legend()
97+
fig.tight_layout()
98+
fig.savefig(out_path)
99+
plt.close(fig)
100+
return out_path
101+
102+
78103
def plot_endpoint_latency(report: ReportDict, out_dir: Path, top_n: int = 10) -> Path:
79104
data = _top_endpoints(report, top_n)
80105
if not data:
@@ -86,24 +111,14 @@ def plot_endpoint_latency(report: ReportDict, out_dir: Path, top_n: int = 10) ->
86111
p90 = [v.get("latency_ms_success", empty_latency).get("p90", 0) for _, v in data]
87112
p99 = [v.get("latency_ms_success", empty_latency).get("p99", 0) for _, v in data]
88113

89-
x = range(len(labels))
90114
width = 0.25
91115

92-
fig, ax = plt.subplots(figsize=(max(10, len(labels) * 0.6), 5))
93-
ax.bar([i - width for i in x], p50, width=width, label="p50", color="#22c55e")
94-
ax.bar(x, p90, width=width, label="p90", color="#eab308")
95-
ax.bar([i + width for i in x], p99, width=width, label="p99", color="#ef4444")
96-
ax.set_ylabel("Latency (ms)")
97-
ax.set_title("Success Latency by Endpoint (Top N)")
98-
ax.set_xticks(list(x))
99-
ax.set_xticklabels(labels, rotation=45, ha="right")
100-
ax.grid(True, axis="y", alpha=0.3)
101-
ax.legend()
102-
out_path = out_dir / "endpoint_latency.png"
103-
fig.tight_layout()
104-
fig.savefig(out_path)
105-
plt.close(fig)
106-
return out_path
116+
def bars(ax: Axes, x: range) -> None:
117+
ax.bar([i - width for i in x], p50, width=width, label="p50", color="#22c55e")
118+
ax.bar(x, p90, width=width, label="p90", color="#eab308")
119+
ax.bar([i + width for i in x], p99, width=width, label="p99", color="#ef4444")
120+
121+
return _save_bar_chart(labels, "Success Latency by Endpoint (Top N)", "Latency (ms)", out_dir / "endpoint_latency.png", bars)
107122

108123

109124
def plot_endpoint_throughput(report: ReportDict, out_dir: Path, top_n: int = 10) -> Path:
@@ -116,23 +131,13 @@ def plot_endpoint_throughput(report: ReportDict, out_dir: Path, top_n: int = 10)
116131
errors = [v.get("errors", 0) for _, v in data]
117132
successes = [t - e for t, e in zip(total, errors)]
118133

119-
x = range(len(labels))
120134
width = 0.45
121135

122-
fig, ax = plt.subplots(figsize=(max(10, len(labels) * 0.6), 5))
123-
ax.bar(x, successes, width=width, label="Success", color="#22c55e")
124-
ax.bar(x, errors, width=width, bottom=successes, label="Errors", color="#ef4444")
125-
ax.set_ylabel("Requests")
126-
ax.set_title("Endpoint Throughput (Top N)")
127-
ax.set_xticks(list(x))
128-
ax.set_xticklabels(labels, rotation=45, ha="right")
129-
ax.grid(True, axis="y", alpha=0.3)
130-
ax.legend()
131-
out_path = out_dir / "endpoint_throughput.png"
132-
fig.tight_layout()
133-
fig.savefig(out_path)
134-
plt.close(fig)
135-
return out_path
136+
def bars(ax: Axes, x: range) -> None:
137+
ax.bar(x, successes, width=width, label="Success", color="#22c55e")
138+
ax.bar(x, errors, width=width, bottom=successes, label="Errors", color="#ef4444")
139+
140+
return _save_bar_chart(labels, "Endpoint Throughput (Top N)", "Requests", out_dir / "endpoint_throughput.png", bars)
136141

137142

138143
def generate_plots(report_path: str | Path, output_dir: str | Path | None = None) -> List[Path]:

0 commit comments

Comments
 (0)