Skip to content
Merged

I #149

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ pformat.txt
profile_slow.prof
.pytest_cache/
*.zip
/_src
/.commandcode
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Three-layer architecture:
```
Layer 1: Transport + Auth (newapi/api_client/)
RequestsHandler (retry/CSRF/maxlag) -> WikiLoginClient (auth, cookies, pagination)
Exceptions: WikiClientError -> LoginError, CSRFError, MaxlagError, MaxRetriesExceeded, CookieError
Exceptions: WikiClientError -> LoginError, CSRFError, MaxlagError, MaxRetriesExceededError, CookieError

Layer 2: Wiki Business Logic (newapi/client_wiki/)
AllAPIS (main facade) -> MainPage (page ops), CategoryDepth (recursive traversal), NewApi (bulk ops)
Expand Down Expand Up @@ -95,7 +95,7 @@ Sessions persisted as Mozilla-format cookie jar files. Auto-expire after 3 days.
### Error Handling

Two parallel exception hierarchies:
1. `api_client/exceptions.py`: `WikiClientError` -> `LoginError`, `CSRFError`, `MaxlagError`, `MaxRetriesExceeded`, `CookieError`
1. `api_client/exceptions.py`: `WikiClientError` -> `LoginError`, `CSRFError`, `MaxlagError`, `MaxRetriesExceededError`, `CookieError`
2. `core/exceptions.py`: `NewApiException` -> `ApiError` -> `AbuseFilterError`, `MaxLagError`, `ArticleExistsError`, `ProtectedPageError`, etc. Includes `parse_api_error()` for mapping API error dicts.

### Patterns to Know
Expand Down
2 changes: 1 addition & 1 deletion PROJECT_AUDIT_REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ The codebase has **9 crash-level bugs** in common code paths, **1 SQL injection

1. **God classes**: `NewApi` (1300 lines), `MainPage` (1000 lines), `WikiLoginClient` (850 lines)
2. **Code duplication**: `client_request_retry` is a copy-paste of `_client_request`
3. **Unused abstractions**: `core/exceptions.py` hierarchy defined but never used; `MaxRetriesExceeded` and `CookieError` defined but never raised
3. **Unused abstractions**: `core/exceptions.py` hierarchy defined but never used; `MaxRetriesExceededError` and `CookieError` defined but never raised
4. **Hardcoded values**: `BOT_USERNAME = "Mr.Ibrahembot"`, `mdwiki.org` special case, Arabic `"قالب:"` prefix
5. **Commented-out code**: Scattered across `db_bot.py`, `bot_api.py`, `page.py`
6. **Outdated config**: `pyproject.toml` references `src_paths = "ArWikiCats"` (non-existent project)
Expand Down
2 changes: 1 addition & 1 deletion newapi/DB_bots/db_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def create_table(self, table_name: str, fields: Dict[str, Any], pk: str = "id",

def query(self, sql: str) -> List[tuple]:
# return self.db.query(sql)
return [r for r in self.db.execute(sql).fetchall()]
return list(self.db.execute(sql).fetchall())

def update(self, sql: str) -> None:
self.db.executescript(sql)
Expand Down
8 changes: 6 additions & 2 deletions newapi/DB_bots/pymysql_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ def sql_connect_pymysql(
query,
return_dict: bool = False,
values=None,
main_args={},
credentials={},
main_args=None,
credentials=None,
conversions=None,
many: bool = False,
**kwargs,
):
if credentials is None:
credentials = {}
if main_args is None:
main_args = {}
args = copy.deepcopy(main_args)
args["cursorclass"] = pymysql.cursors.DictCursor if return_dict else pymysql.cursors.Cursor
if conversions:
Expand Down
79 changes: 41 additions & 38 deletions newapi/api_client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ The `api_client` package provides a robust, authenticated HTTP client for the Me

### Main Modules

| Module | Purpose |
|---|---|
| `client.py` | Core client classes: `RequestsHandler` (retry/transport), `CookiesClient` (cookie I/O), `WikiLoginClient` (business layer) |
| `cookies.py` | Cookie file path resolution, staleness checks, and cleanup |
| `exceptions.py` | Custom exception hierarchy rooted at `WikiClientError` |
| Module | Purpose |
| --------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `client.py` | Core client classes: `RequestsHandler` (retry/transport), `CookiesClient` (cookie I/O), `WikiLoginClient` (business layer) |
| `cookies.py` | Cookie file path resolution, staleness checks, and cleanup |
| `exceptions.py` | Custom exception hierarchy rooted at `WikiClientError` |

### Technologies & Dependencies

- **`mwclient`** -- MediaWiki client library (provides `Site` and session management)
- **`requests`** -- HTTP transport (used internally by mwclient)
- **`http.cookiejar.LWPCookieJar`** -- Mozilla-format cookie persistence
- **Standard library:** `copy`, `logging`, `os`, `stat`, `time`, `datetime`, `pathlib`
- **`mwclient`** -- MediaWiki client library (provides `Site` and session management)
- **`requests`** -- HTTP transport (used internally by mwclient)
- **`http.cookiejar.LWPCookieJar`** -- Mozilla-format cookie persistence
- **Standard library:** `copy`, `logging`, `os`, `stat`, `time`, `datetime`, `pathlib`

---

Expand All @@ -33,10 +33,10 @@ The package follows a layered architecture:

### Design Patterns

- **Template Method Pattern**: `RequestsHandler` defines the retry loop skeleton and calls abstract hooks (`_session`, `_refresh_csrf_token`, `_on_assertnameduserfailed`) that `WikiLoginClient` implements.
- **Mixin / Multiple Inheritance**: `WikiLoginClient` inherits from both `CookiesClient` and `RequestsHandler`.
- **Facade**: Wraps `mwclient.Site` and `requests.Session` behind a simplified interface.
- **Strategy-like error handling**: The retry loop dispatches on error code to distinct handlers (CSRF, maxlag, assertnameduserfailed, ratelimited).
- **Template Method Pattern**: `RequestsHandler` defines the retry loop skeleton and calls abstract hooks (`_session`, `_refresh_csrf_token`, `_on_assertnameduserfailed`) that `WikiLoginClient` implements.
- **Mixin / Multiple Inheritance**: `WikiLoginClient` inherits from both `CookiesClient` and `RequestsHandler`.
- **Facade**: Wraps `mwclient.Site` and `requests.Session` behind a simplified interface.
- **Strategy-like error handling**: The retry loop dispatches on error code to distinct handlers (CSRF, maxlag, assertnameduserfailed, ratelimited).

### Maintainability

Expand All @@ -54,21 +54,21 @@ The client is single-threaded with no connection pooling configuration beyond wh

## Strengths

- **Robust retry logic**: Handles CSRF token invalidation, maxlag, rate limiting, and session expiry with configurable retry counts and exponential backoff.
- **Cookie persistence**: Sessions survive process restarts via LWPCookieJar with automatic staleness invalidation (3-day max age).
- **Write-action safety**: Automatically injects `bot=1` and `assertuser` parameters for mutating API requests.
- **Clean exception hierarchy**: Well-structured custom exceptions with a common base class.
- **Continuation pagination**: `post_continue` handles multi-page API responses transparently.
- **Robust retry logic**: Handles CSRF token invalidation, maxlag, rate limiting, and session expiry with configurable retry counts and exponential backoff.
- **Cookie persistence**: Sessions survive process restarts via LWPCookieJar with automatic staleness invalidation (3-day max age).
- **Write-action safety**: Automatically injects `bot=1` and `assertuser` parameters for mutating API requests.
- **Clean exception hierarchy**: Well-structured custom exceptions with a common base class.
- **Continuation pagination**: `post_continue` handles multi-page API responses transparently.

---

## Weaknesses

- **Code duplication**: `client_request_retry` (lines 693-766) is a copy-paste of `_client_request` logic. Any bug fix in one must be manually replicated in the other.
- **Unused exceptions**: `MaxRetriesExceeded` and `CookieError` are defined and exported but never raised anywhere in the codebase.
- **Shadowed builtins**: The `max` parameter in `post_continue` shadows Python's built-in `max()`.
- **Hardcoded domain workaround**: `mdwiki.org` special case baked into the general-purpose client (line 442).
- **Unused `**kwargs`**: All three request methods accept `**kwargs` but never pass them downstream.
- **Code duplication**: `client_request_retry` (lines 693-766) is a copy-paste of `_client_request` logic. Any bug fix in one must be manually replicated in the other.
- **Unused exceptions**: `MaxRetriesExceededError` and `CookieError` are defined and exported but never raised anywhere in the codebase.
- **Shadowed builtins**: The `max` parameter in `post_continue` shadows Python's built-in `max()`.
- **Hardcoded domain workaround**: `mdwiki.org` special case baked into the general-purpose client (line 442).
- **Unused `**kwargs`**: All three request methods accept `\*\*kwargs` but never pass them downstream.

---

Expand Down Expand Up @@ -116,26 +116,28 @@ Uses `client_request_safe` for continuation pages. If a continuation page fails,

## Areas That Need Attention

- **Missing HTTP timeout**: Add `timeout=(connect, read)` to all HTTP requests.
- **Missing `__init__.py` exports**: `_delete_cookie_file` is a private-underscore function imported across modules -- rename or make public.
- **No context manager**: The client holds resources (`requests.Session`, cookie jar) but provides no `__enter__`/`__exit__` for clean release.
- **No abstract base class**: `RequestsHandler` uses `raise NotImplementedError` instead of `abc.ABC` with `@abstractmethod`.
- **No input validation**: `lang` and `family` parameters are not validated -- empty strings or special characters produce malformed URLs.
- **No structured error context**: Exceptions carry only a message string. Adding `error_code`, `url`, `attempt_count` fields would aid debugging.
- **f-string in logger call** (line 575): Inconsistent with the rest of the module's `%s` formatting and defeats lazy evaluation.
- **Missing HTTP timeout**: Add `timeout=(connect, read)` to all HTTP requests.
- **Missing `__init__.py` exports**: `_delete_cookie_file` is a private-underscore function imported across modules -- rename or make public.
- **No context manager**: The client holds resources (`requests.Session`, cookie jar) but provides no `__enter__`/`__exit__` for clean release.
- **No abstract base class**: `RequestsHandler` uses `raise NotImplementedError` instead of `abc.ABC` with `@abstractmethod`.
- **No input validation**: `lang` and `family` parameters are not validated -- empty strings or special characters produce malformed URLs.
- **No structured error context**: Exceptions carry only a message string. Adding `error_code`, `url`, `attempt_count` fields would aid debugging.
- **f-string in logger call** (line 575): Inconsistent with the rest of the module's `%s` formatting and defeats lazy evaluation.

---

## Improvement Plan

### Quick Wins

1. Increment `attempt` in the `ratelimited` handler to prevent infinite loops.
2. Add `timeout` parameter to `_execute_request`.
3. Remove `client_request_retry` duplication -- delegate to `_client_request`.
4. Raise unused exceptions (`MaxRetriesExceeded`, `CookieError`) or remove them.
4. Raise unused exceptions (`MaxRetriesExceededError`, `CookieError`) or remove them.
5. Fix the f-string logger call to use `%s` formatting.

### Medium-Term Improvements

1. Implement `_ensure_logged_in` to actually trigger login when cookies are absent.
2. Add `__enter__`/`__exit__` context manager support.
3. Use `abc.ABC` and `@abstractmethod` for `RequestsHandler`.
Expand All @@ -144,6 +146,7 @@ Uses `client_request_safe` for continuation pages. If a continuation page fails,
6. Make the `mdwiki.org` workaround configurable.

### Long-Term Refactoring

1. Split `client.py` into separate modules: `transport.py`, `auth.py`, `pagination.py`.
2. Add structured error context to all exceptions.
3. Configure `urllib3.util.retry.Retry` and `HTTPAdapter` at the transport level.
Expand All @@ -154,10 +157,10 @@ Uses `client_request_safe` for continuation pages. If a continuation page fails,

## Comprehensive Review

| Metric | Score |
|---|---|
| **Overall Rating** | **6.5/10** |
| **Production Readiness** | Moderate -- functional but has infinite loop bug and missing timeouts |
| **Technical Debt** | Medium -- code duplication, unused exceptions, hardcoded workarounds |
| **Risk Assessment** | Medium-High -- the ratelimited infinite loop and missing timeouts are production risks |
| **Maintainability** | 6/10 -- clear architecture but duplicated code and 850+ line file |
| Metric | Score |
| ------------------------ | -------------------------------------------------------------------------------------- |
| **Overall Rating** | **6.5/10** |
| **Production Readiness** | Moderate -- functional but has infinite loop bug and missing timeouts |
| **Technical Debt** | Medium -- code duplication, unused exceptions, hardcoded workarounds |
| **Risk Assessment** | Medium-High -- the ratelimited infinite loop and missing timeouts are production risks |
| **Maintainability** | 6/10 -- clear architecture but duplicated code and 850+ line file |
13 changes: 6 additions & 7 deletions newapi/api_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
# api_client/__init__.py
# Public surface of the package.
# Import WikiLoginClient and all exception types from here.

from .client import WikiLoginClient
from .cookies_client import CookiesClient
from .exceptions import (
CookieError,
CSRFError,
LoginError,
MaxlagError,
MaxRetriesExceeded,
MaxRetriesExceededError,
WikiClientError,
)
from .requests_handler import RequestsHandler

__all__ = [
"WikiLoginClient",
# Exceptions
"RequestsHandler",
"CookiesClient",
"WikiClientError",
"LoginError",
"CSRFError",
"MaxlagError",
"MaxRetriesExceeded",
"MaxRetriesExceededError",
"CookieError",
]
Loading
Loading