Skip to content

Commit 4ff8c98

Browse files
chore: expand linting rules and harden adapters/tests
1 parent a7fd76c commit 4ff8c98

34 files changed

+323
-187
lines changed

.cursor/rules/checks.mdc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,23 +168,46 @@ class ElasticsearchConfig(BaseModel):
168168
**Enabled Rules**:
169169
- A: flake8-builtins
170170
- ANN: flake8-annotations
171+
- ARG: flake8-unused-arguments
171172
- ASYNC: flake8-async
172173
- B: flake8-bugbear
174+
- C4: flake8-comprehensions
173175
- D: pydocstyle (Google-style, except D100, D104, D107)
174176
- E/W: pycodestyle (except E501)
177+
- EM: flake8-errmsg
175178
- ERA: eradicate
176179
- F: pyflakes (except F811 in steps)
180+
- FAST: FastAPI best practices
181+
- FURB: refurb (modern Python patterns)
177182
- G: logging (allow G004 f-strings)
178183
- I: isort (future → stdlib → third-party → first-party → local)
184+
- ICN: flake8-import-conventions
185+
- LOG: flake8-logging
186+
- PERF: Perflint (performance)
187+
- PIE: flake8-pie
188+
- PL: Pylint (PLC, PLE, PLR, PLW)
189+
- PTH: flake8-use-pathlib
190+
- PYI: flake8-pyi (stub files)
179191
- RUF: Ruff rules (except RUF001/RUF003 for Persian)
180192
- S: Bandit (allow S101 assert, S301/S403 pickle)
193+
- SIM: flake8-simplify
194+
- SLOT: flake8-slots
195+
- T10: flake8-debugger
196+
- T20: flake8-print
197+
- TC: flake8-type-checking
198+
- TID: flake8-tidy-imports
181199
- TRY: tryceratops (except TRY003, TRY301)
182200
- UP: pyupgrade
183201

184202
**Ignored**:
185203
- C901: McCabe max 10
204+
- EM101/EM102: Exception message formatting
186205
- F811: Redefinition in steps
187206
- G004: f-strings in logging
207+
- PERF203: try-except in loop
208+
- PLR0904/PLR0914/PLR1702: Complexity thresholds
209+
- SIM102/SIM108: Simplification preferences
210+
- TC001/TC003: TYPE_CHECKING imports (Pydantic compatibility)
188211

189212
## Development Tools
190213

archipy/adapters/base/sqlalchemy/adapters.py

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,21 @@ class SQLAlchemyFilterMixin:
7979
Supports equality, inequality, string operations, list operations, and NULL checks.
8080
"""
8181

82+
@staticmethod
83+
def _validate_list_operation(
84+
value: str | float | bool | list | UUID | None,
85+
operation: FilterOperationType,
86+
) -> list:
87+
"""Validate that value is a list for list operations."""
88+
if not isinstance(value, list):
89+
raise InvalidArgumentError(f"{operation.value} operation requires a list, got {type(value)}")
90+
return value
91+
8292
@staticmethod
8393
def _apply_filter(
8494
query: Select | Update | Delete,
8595
field: InstrumentedAttribute,
86-
value: str | int | float | bool | list | UUID | None,
96+
value: str | float | bool | list | UUID | None,
8797
operation: FilterOperationType,
8898
) -> Select | Update | Delete:
8999
"""Apply a filter to a SQLAlchemy query based on the specified operation.
@@ -97,42 +107,36 @@ def _apply_filter(
97107
Returns:
98108
The updated query with the filter applied.
99109
"""
100-
if value is not None or operation in [FilterOperationType.IS_NULL, FilterOperationType.IS_NOT_NULL]:
101-
match operation:
102-
case FilterOperationType.EQUAL:
103-
return query.where(field == value)
104-
case FilterOperationType.NOT_EQUAL:
105-
return query.where(field != value)
106-
case FilterOperationType.LESS_THAN:
107-
return query.where(field < value)
108-
case FilterOperationType.LESS_THAN_OR_EQUAL:
109-
return query.where(field <= value)
110-
case FilterOperationType.GREATER_THAN:
111-
return query.where(field > value)
112-
case FilterOperationType.GREATER_THAN_OR_EQUAL:
113-
return query.where(field >= value)
114-
case FilterOperationType.IN_LIST:
115-
if not isinstance(value, list):
116-
raise InvalidArgumentError(f"IN_LIST operation requires a list, got {type(value)}")
117-
return query.where(field.in_(value))
118-
case FilterOperationType.NOT_IN_LIST:
119-
if not isinstance(value, list):
120-
raise InvalidArgumentError(f"NOT_IN_LIST operation requires a list, got {type(value)}")
121-
return query.where(~field.in_(value))
122-
case FilterOperationType.LIKE:
123-
return query.where(field.like(f"%{value}%"))
124-
case FilterOperationType.ILIKE:
125-
return query.where(field.ilike(f"%{value}%"))
126-
case FilterOperationType.STARTS_WITH:
127-
return query.where(field.startswith(value))
128-
case FilterOperationType.ENDS_WITH:
129-
return query.where(field.endswith(value))
130-
case FilterOperationType.CONTAINS:
131-
return query.where(field.contains(value))
132-
case FilterOperationType.IS_NULL:
133-
return query.where(field.is_(None))
134-
case FilterOperationType.IS_NOT_NULL:
135-
return query.where(field.isnot(None))
110+
# Skip filter if value is None (except for IS_NULL/IS_NOT_NULL operations)
111+
if value is None and operation not in [FilterOperationType.IS_NULL, FilterOperationType.IS_NOT_NULL]:
112+
return query
113+
114+
# Map operations to their corresponding SQLAlchemy expressions
115+
filter_map = {
116+
FilterOperationType.EQUAL: lambda: field == value,
117+
FilterOperationType.NOT_EQUAL: lambda: field != value,
118+
FilterOperationType.LESS_THAN: lambda: field < value,
119+
FilterOperationType.LESS_THAN_OR_EQUAL: lambda: field <= value,
120+
FilterOperationType.GREATER_THAN: lambda: field > value,
121+
FilterOperationType.GREATER_THAN_OR_EQUAL: lambda: field >= value,
122+
FilterOperationType.IN_LIST: lambda: field.in_(
123+
SQLAlchemyFilterMixin._validate_list_operation(value, operation),
124+
),
125+
FilterOperationType.NOT_IN_LIST: lambda: ~field.in_(
126+
SQLAlchemyFilterMixin._validate_list_operation(value, operation),
127+
),
128+
FilterOperationType.LIKE: lambda: field.like(f"%{value}%"),
129+
FilterOperationType.ILIKE: lambda: field.ilike(f"%{value}%"),
130+
FilterOperationType.STARTS_WITH: lambda: field.startswith(value),
131+
FilterOperationType.ENDS_WITH: lambda: field.endswith(value),
132+
FilterOperationType.CONTAINS: lambda: field.contains(value),
133+
FilterOperationType.IS_NULL: lambda: field.is_(None),
134+
FilterOperationType.IS_NOT_NULL: lambda: field.isnot(None),
135+
}
136+
137+
filter_expr = filter_map.get(operation)
138+
if filter_expr:
139+
return query.where(filter_expr())
136140
return query
137141

138142

archipy/adapters/base/sqlalchemy/session_managers.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ def _expected_config_type(self) -> type[SQLAlchemyConfig]:
7171
Returns:
7272
The SQLAlchemy configuration class expected by this session manager.
7373
"""
74-
pass
7574

7675
@abstractmethod
7776
def _get_database_name(self) -> str:
@@ -80,7 +79,6 @@ def _get_database_name(self) -> str:
8079
Returns:
8180
str: The name of the database (e.g., 'postgresql', 'sqlite', 'starrocks').
8281
"""
83-
pass
8482

8583
@abstractmethod
8684
def _create_url(self, configs: ConfigT) -> URL:
@@ -95,7 +93,6 @@ def _create_url(self, configs: ConfigT) -> URL:
9593
Raises:
9694
DatabaseConnectionError: If there's an error creating the URL.
9795
"""
98-
pass
9996

10097
def _create_engine(self, configs: ConfigT) -> Engine:
10198
"""Create a SQLAlchemy engine with common configuration.
@@ -257,7 +254,6 @@ def _expected_config_type(self) -> type[SQLAlchemyConfig]:
257254
Returns:
258255
The SQLAlchemy configuration class expected by this session manager.
259256
"""
260-
pass
261257

262258
@abstractmethod
263259
def _get_database_name(self) -> str:
@@ -266,7 +262,6 @@ def _get_database_name(self) -> str:
266262
Returns:
267263
str: The name of the database (e.g., 'postgresql', 'sqlite', 'starrocks').
268264
"""
269-
pass
270265

271266
@abstractmethod
272267
def _create_url(self, configs: ConfigT) -> URL:
@@ -281,7 +276,6 @@ def _create_url(self, configs: ConfigT) -> URL:
281276
Raises:
282277
DatabaseConnectionError: If there's an error creating the URL.
283278
"""
284-
pass
285279

286280
def _create_async_engine(self, configs: ConfigT) -> AsyncEngine:
287281
"""Create an async SQLAlchemy engine with common configuration.

archipy/adapters/email/adapters.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from email.mime.image import MIMEImage
1010
from email.mime.multipart import MIMEMultipart
1111
from email.mime.text import MIMEText
12+
from pathlib import Path
1213
from queue import Queue
1314
from typing import BinaryIO, override
1415

@@ -24,6 +25,8 @@
2425
from archipy.models.errors import InvalidArgumentError
2526
from archipy.models.types.email_types import EmailAttachmentDispositionType, EmailAttachmentType
2627

28+
logger = logging.getLogger(__name__)
29+
2730

2831
class EmailConnectionManager:
2932
"""Manages SMTP connections with connection pooling and timeout handling."""
@@ -135,10 +138,10 @@ def create_attachment(
135138
def _process_source(source: str | bytes | BinaryIO | HttpUrl, attachment_type: EmailAttachmentType) -> bytes:
136139
"""Process different types of attachment sources."""
137140
if attachment_type == EmailAttachmentType.FILE:
138-
if isinstance(source, (str, os.PathLike)):
139-
file_path = os.fspath(source)
140-
with open(file_path, "rb") as f:
141-
return f.read()
141+
if isinstance(source, str):
142+
return Path(source).read_bytes()
143+
if isinstance(source, os.PathLike):
144+
return Path(os.fspath(source)).read_bytes()
142145
raise ValueError(f"File attachment type requires string path, got {type(source)}")
143146
elif attachment_type == EmailAttachmentType.BASE64:
144147
if isinstance(source, str | bytes):
@@ -252,7 +255,7 @@ def send_email(
252255
try:
253256
if connection.smtp_connection:
254257
connection.smtp_connection.send_message(msg, to_addrs=recipients)
255-
logging.debug(f"Email sent successfully to {to_email}")
258+
logger.debug(f"Email sent successfully to {to_email}")
256259
return
257260
else:
258261
connection.connect()
@@ -300,7 +303,7 @@ def _create_message(
300303
# Treat as file path
301304
attachment_obj = AttachmentHandler.create_attachment(
302305
source=attachment,
303-
filename=os.path.basename(attachment),
306+
filename=Path(attachment).name,
304307
attachment_type=EmailAttachmentType.FILE,
305308
)
306309
else:

archipy/adapters/kafka/adapters.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import logging
23
from typing import override
34

@@ -50,10 +51,8 @@ def _handle_kafka_exception(cls, exception: Exception, operation: str) -> None:
5051
# Extract timeout value if available
5152
timeout = None
5253
if hasattr(exception, "args") and len(exception.args) > 1:
53-
try:
54+
with contextlib.suppress(IndexError, ValueError):
5455
timeout = int(exception.args[1])
55-
except (IndexError, ValueError):
56-
pass
5756
raise ConnectionTimeoutError(service="Kafka", timeout=timeout) from exception
5857

5958
# Network/connectivity errors

archipy/adapters/scylladb/adapters.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def _create_cluster(self) -> Any:
209209
port=self.config.PORT,
210210
auth_provider=auth_provider,
211211
protocol_version=self.config.PROTOCOL_VERSION,
212-
compression=True if self.config.COMPRESSION else False,
212+
compression=bool(self.config.COMPRESSION),
213213
connect_timeout=self.config.CONNECT_TIMEOUT,
214214
load_balancing_policy=load_balancing_policy,
215215
default_retry_policy=retry_policy,
@@ -405,7 +405,7 @@ def insert(self, table: str, data: dict[str, Any], ttl: int | None = None, if_no
405405
This prevents errors on duplicate primary keys but is slow
406406
"""
407407
columns = ", ".join(data.keys())
408-
placeholders = ", ".join(["%s" for _ in data.keys()])
408+
placeholders = ", ".join(["%s" for _ in data])
409409
query = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
410410

411411
if if_not_exists:
@@ -442,7 +442,7 @@ def select(
442442

443443
params = None
444444
if conditions:
445-
where_clause = " AND ".join([f"{key} = %s" for key in conditions.keys()])
445+
where_clause = " AND ".join([f"{key} = %s" for key in conditions])
446446
query += f" WHERE {where_clause}"
447447
params = tuple(conditions.values())
448448

@@ -463,8 +463,8 @@ def update(self, table: str, data: dict[str, Any], conditions: dict[str, Any], t
463463
conditions (dict[str, Any]): WHERE clause conditions as key-value pairs.
464464
ttl (int | None): Time to live in seconds. If None, data persists indefinitely.
465465
"""
466-
set_clause = ", ".join([f"{key} = %s" for key in data.keys()])
467-
where_clause = " AND ".join([f"{key} = %s" for key in conditions.keys()])
466+
set_clause = ", ".join([f"{key} = %s" for key in data])
467+
where_clause = " AND ".join([f"{key} = %s" for key in conditions])
468468
query = f"UPDATE {table}"
469469

470470
if ttl is not None:
@@ -489,7 +489,7 @@ def delete(self, table: str, conditions: dict[str, Any]) -> None:
489489
table (str): The name of the table.
490490
conditions (dict[str, Any]): WHERE clause conditions as key-value pairs.
491491
"""
492-
where_clause = " AND ".join([f"{key} = %s" for key in conditions.keys()])
492+
where_clause = " AND ".join([f"{key} = %s" for key in conditions])
493493
query = f"DELETE FROM {table} WHERE {where_clause}"
494494

495495
try:
@@ -610,7 +610,7 @@ def count(self, table: str, conditions: dict[str, Any] | None = None) -> int:
610610

611611
params = None
612612
if conditions:
613-
where_clause = " AND ".join([f"{key} = %s" for key in conditions.keys()])
613+
where_clause = " AND ".join([f"{key} = %s" for key in conditions])
614614
query += f" WHERE {where_clause} ALLOW FILTERING"
615615
params = tuple(conditions.values())
616616

@@ -634,7 +634,7 @@ def exists(self, table: str, conditions: dict[str, Any]) -> bool:
634634
Returns:
635635
bool: True if at least one row exists, False otherwise.
636636
"""
637-
where_clause = " AND ".join([f"{key} = %s" for key in conditions.keys()])
637+
where_clause = " AND ".join([f"{key} = %s" for key in conditions])
638638
query = f"SELECT COUNT(*) FROM {table} WHERE {where_clause} LIMIT 1 ALLOW FILTERING"
639639

640640
try:
@@ -792,7 +792,7 @@ def _create_cluster(self) -> Any:
792792
port=self.config.PORT,
793793
auth_provider=auth_provider,
794794
protocol_version=self.config.PROTOCOL_VERSION,
795-
compression=True if self.config.COMPRESSION else False,
795+
compression=bool(self.config.COMPRESSION),
796796
connect_timeout=self.config.CONNECT_TIMEOUT,
797797
load_balancing_policy=load_balancing_policy,
798798
default_retry_policy=retry_policy,
@@ -1008,7 +1008,7 @@ async def insert(
10081008
This prevents errors on duplicate primary keys but is slow
10091009
"""
10101010
columns = ", ".join(data.keys())
1011-
placeholders = ", ".join(["%s" for _ in data.keys()])
1011+
placeholders = ", ".join(["%s" for _ in data])
10121012
query = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
10131013

10141014
if if_not_exists:
@@ -1045,7 +1045,7 @@ async def select(
10451045

10461046
params = None
10471047
if conditions:
1048-
where_clause = " AND ".join([f"{key} = %s" for key in conditions.keys()])
1048+
where_clause = " AND ".join([f"{key} = %s" for key in conditions])
10491049
query += f" WHERE {where_clause}"
10501050
params = tuple(conditions.values())
10511051

@@ -1072,8 +1072,8 @@ async def update(
10721072
conditions (dict[str, Any]): WHERE clause conditions as key-value pairs.
10731073
ttl (int | None): Time to live in seconds. If None, data persists indefinitely.
10741074
"""
1075-
set_clause = ", ".join([f"{key} = %s" for key in data.keys()])
1076-
where_clause = " AND ".join([f"{key} = %s" for key in conditions.keys()])
1075+
set_clause = ", ".join([f"{key} = %s" for key in data])
1076+
where_clause = " AND ".join([f"{key} = %s" for key in conditions])
10771077
query = f"UPDATE {table}"
10781078

10791079
if ttl is not None:
@@ -1098,7 +1098,7 @@ async def delete(self, table: str, conditions: dict[str, Any]) -> None:
10981098
table (str): The name of the table.
10991099
conditions (dict[str, Any]): WHERE clause conditions as key-value pairs.
11001100
"""
1101-
where_clause = " AND ".join([f"{key} = %s" for key in conditions.keys()])
1101+
where_clause = " AND ".join([f"{key} = %s" for key in conditions])
11021102
query = f"DELETE FROM {table} WHERE {where_clause}"
11031103

11041104
try:
@@ -1225,7 +1225,7 @@ async def count(self, table: str, conditions: dict[str, Any] | None = None) -> i
12251225

12261226
params = None
12271227
if conditions:
1228-
where_clause = " AND ".join([f"{key} = %s" for key in conditions.keys()])
1228+
where_clause = " AND ".join([f"{key} = %s" for key in conditions])
12291229
query += f" WHERE {where_clause} ALLOW FILTERING"
12301230
params = tuple(conditions.values())
12311231

@@ -1249,7 +1249,7 @@ async def exists(self, table: str, conditions: dict[str, Any]) -> bool:
12491249
Returns:
12501250
bool: True if at least one row exists, False otherwise.
12511251
"""
1252-
where_clause = " AND ".join([f"{key} = %s" for key in conditions.keys()])
1252+
where_clause = " AND ".join([f"{key} = %s" for key in conditions])
12531253
query = f"SELECT COUNT(*) FROM {table} WHERE {where_clause} LIMIT 1 ALLOW FILTERING"
12541254

12551255
try:

0 commit comments

Comments
 (0)