refactor: convert f-string logging to lazy %-style format (G004)

Sweep all 1676 G004 violations across 112 files, converting
`logger.<level>(f"…{x}…")` to `logger.<level>("…%s…", x)`.

Why: ruff rule G004 was added to pyproject.toml to enforce lazy
%-style logging — defers formatting until the log level is enabled
and lets structured log tooling match the unformatted template.

Conversion preserves rendered output byte-for-byte:
- `{x}` → `%s` + `x`
- `{x!r}` / `{x!s}` / `{x!a}` → `%r` / `%s` / `%a`
- Format specs (`{x:.2f}`, `{x:>10}`) → `%s` + `format(x, 'spec')`
  (printf-style specs aren't 1:1 with Python format specs, so we
  delegate to `format()` to keep identical output)
- Literal `%` → `%%`
- Concatenated f-strings (`f"a {x} " "b"`) flattened
- Trailing kwargs (`exc_info=True`) preserved

Verified:
- `uv run ruff check --select G004` → 0 violations
- `uv run ty check -- nextcloud_mcp_server` → passes
- `uv run pytest tests/unit/` → 1010 passed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-13 01:12:17 +02:00
co-authored by Claude Opus 4.7
parent a4e6125d28
commit 665cb9b1eb
112 changed files with 2534 additions and 1859 deletions
+1 -1
View File
@@ -167,7 +167,7 @@ class NextcloudClient:
"""
from ..auth import BearerAuth # noqa: PLC0415
logger.info(f"Creating NC Client for user '{username}' using OAuth token")
logger.info("Creating NC Client for user '%s' using OAuth token", username)
return cls(
base_url=base_url,
username=username,
+15 -5
View File
@@ -43,7 +43,8 @@ def retry_on_429(func):
# error we wait a couple of seconds and do a retry
if e.response.status_code == codes.TOO_MANY_REQUESTS:
logger.warning(
f"429 Client Error: Too Many Requests, Number of attempts: {retries}"
"429 Client Error: Too Many Requests, Number of attempts: %s",
retries,
)
# Record retry metric (extract app name from args if available)
if len(args) > 0 and hasattr(args[0], "app_name"):
@@ -53,17 +54,26 @@ def retry_on_429(func):
# 404 errors are often expected (e.g., checking if attachments exist)
# Log as debug instead of warning
logger.debug(
f"HTTPStatusError {e.response.status_code}: {e}, Number of attempts: {retries}"
"HTTPStatusError %s: %s, Number of attempts: %s",
e.response.status_code,
e,
retries,
)
raise
else:
logger.warning(
f"HTTPStatusError {e.response.status_code}: {e}, Number of attempts: {retries}"
"HTTPStatusError %s: %s, Number of attempts: %s",
e.response.status_code,
e,
retries,
)
raise
except RequestError as e:
logger.warning(
f"RequestError {e.request.url}: {e}, Number of attempts: {retries}"
"RequestError %s: %s, Number of attempts: %s",
e.request.url,
e,
retries,
)
raise
@@ -127,7 +137,7 @@ class BaseNextcloudClient(ABC):
Response object
"""
url = self._resolve_url(url)
logger.debug(f"Making {method} request to {url}")
logger.debug("Making %s request to %s", method, url)
# Start timer for metrics
start_time = time.time()
+50 -39
View File
@@ -131,23 +131,32 @@ class CalendarClient:
max_attempts: Maximum polling attempts (default: 40)
initial_delay_ms: Initial delay between attempts in ms (default: 100ms)
"""
logger.info(f"Waiting for calendar '{calendar_name}' to propagate...")
logger.info("Waiting for calendar '%s' to propagate...", calendar_name)
delay_ms = initial_delay_ms
for attempt in range(max_attempts):
try:
logger.debug(
f"Attempt {attempt + 1}/{max_attempts} to find calendar '{calendar_name}'..."
"Attempt %s/%s to find calendar '%s'...",
attempt + 1,
max_attempts,
calendar_name,
)
calendars = await self.list_calendars()
if any(cal["name"] == calendar_name for cal in calendars):
logger.info(
f"Calendar '{calendar_name}' became available after {attempt + 1} attempts"
"Calendar '%s' became available after %s attempts",
calendar_name,
attempt + 1,
)
return
except Exception as e:
logger.warning(
f"Attempt {attempt + 1}/{max_attempts} to verify calendar '{calendar_name}' failed: {e}"
"Attempt %s/%s to verify calendar '%s' failed: %s",
attempt + 1,
max_attempts,
calendar_name,
e,
)
if attempt < max_attempts - 1:
@@ -156,7 +165,9 @@ class CalendarClient:
delay_ms = min(delay_ms * 2, 2000)
logger.error(
f"Calendar '{calendar_name}' did not become available after {max_attempts} attempts."
"Calendar '%s' did not become available after %s attempts.",
calendar_name,
max_attempts,
)
# ============= Calendar Operations =============
@@ -243,7 +254,7 @@ class CalendarClient:
}
)
logger.debug(f"Found {len(result)} calendars")
logger.debug("Found %s calendars", len(result))
return result
async def create_calendar(
@@ -285,7 +296,7 @@ class CalendarClient:
f"Failed to create calendar '{calendar_name}': HTTP {response.status}"
)
logger.debug(f"Created calendar: {calendar_name}")
logger.debug("Created calendar: %s", calendar_name)
# Wait for calendar to be queryable (Nextcloud eventual consistency)
await self._wait_for_calendar_propagation(calendar_name)
@@ -306,7 +317,7 @@ class CalendarClient:
)
await self._dav_client.delete(calendar_url)
logger.debug(f"Deleted calendar: {calendar_name}")
logger.debug("Deleted calendar: %s", calendar_name)
return {"status_code": 204}
# ============= Event Operations =============
@@ -467,7 +478,7 @@ class CalendarClient:
# caldav v3's _async_put raises PutError on HTTP failure
event = await calendar.save_event(ical=ical_content) # type: ignore[misc] # dual-mode
logger.debug(f"Created event {event_uid}")
logger.debug("Created event %s", event_uid)
return {
"uid": event_uid,
@@ -498,7 +509,7 @@ class CalendarClient:
await _maybe_await(event.save())
logger.debug(f"Updated event {event_uid}")
logger.debug("Updated event %s", event_uid)
return {
"uid": event_uid,
"href": str(event.url),
@@ -515,10 +526,10 @@ class CalendarClient:
calendar, event_uid, cdav.CompFilter("VEVENT")
)
await _maybe_await(event.delete())
logger.debug(f"Deleted event {event_uid}")
logger.debug("Deleted event %s", event_uid)
return {"status_code": 204}
except caldav_error.NotFoundError as e:
logger.debug(f"Event {event_uid} not found: {e}")
logger.debug("Event %s not found: %s", event_uid, e)
return {"status_code": 404}
async def get_event(
@@ -539,7 +550,7 @@ class CalendarClient:
event_data["href"] = str(event.url)
event_data["etag"] = ""
logger.debug(f"Retrieved event {event_uid}")
logger.debug("Retrieved event %s", event_uid)
return event_data, ""
async def search_events_across_calendars(
@@ -573,14 +584,14 @@ class CalendarClient:
all_events.extend(events)
except Exception as e:
logger.warning(
f"Error getting events from calendar {calendar['name']}: {e}"
"Error getting events from calendar %s: %s", calendar["name"], e
)
continue
return all_events
except Exception as e:
logger.error(f"Error searching events across calendars: {e}")
logger.error("Error searching events across calendars: %s", e)
raise
# ============= Todo/Task Operations (NEW) =============
@@ -611,7 +622,7 @@ class CalendarClient:
if not filters or self._todo_matches_filters(todo_dict, filters):
result.append(todo_dict)
logger.debug(f"Found {len(result)} todos")
logger.debug("Found %s todos", len(result))
return result
async def create_todo(
@@ -626,7 +637,7 @@ class CalendarClient:
# caldav v3's _async_put raises PutError on HTTP failure
todo = await calendar.save_todo(ical=ical_content) # type: ignore[misc] # dual-mode
logger.debug(f"Created todo {todo_uid}")
logger.debug("Created todo %s", todo_uid)
return {
"uid": todo_uid,
@@ -653,7 +664,7 @@ class CalendarClient:
await _maybe_await(todo.load(only_if_unloaded=True))
logger.debug(
f"Loaded todo {todo_uid}, current data length: {len(todo.data)}" # type: ignore
"Loaded todo %s, current data length: %s", todo_uid, len(todo.data)
)
# Merge updates into existing iCal data
@@ -662,14 +673,14 @@ class CalendarClient:
todo_data,
todo_uid,
)
logger.debug(f"Merged iCal data length: {len(updated_ical)}")
logger.debug(f"Updated iCal content:\n{updated_ical}")
logger.debug("Merged iCal data length: %s", len(updated_ical))
logger.debug("Updated iCal content:\\n%s", updated_ical)
todo.data = updated_ical
await _maybe_await(todo.save())
logger.debug(f"Updated todo {todo_uid}")
logger.debug("Updated todo %s", todo_uid)
return {
"uid": todo_uid,
"href": str(todo.url),
@@ -677,7 +688,7 @@ class CalendarClient:
"status_code": 200,
}
except Exception as e:
logger.error(f"Error updating todo {todo_uid}: {e}", exc_info=True)
logger.error("Error updating todo %s: %s", todo_uid, e, exc_info=True)
raise
async def delete_todo(self, calendar_name: str, todo_uid: str) -> dict[str, Any]:
@@ -689,10 +700,10 @@ class CalendarClient:
calendar, todo_uid, cdav.CompFilter("VTODO")
)
await _maybe_await(todo.delete())
logger.debug(f"Deleted todo {todo_uid}")
logger.debug("Deleted todo %s", todo_uid)
return {"status_code": 204}
except caldav_error.NotFoundError as e:
logger.debug(f"Todo {todo_uid} not found: {e}")
logger.debug("Todo %s not found: %s", todo_uid, e)
return {"status_code": 404}
async def search_todos_across_calendars(
@@ -717,14 +728,14 @@ class CalendarClient:
all_todos.extend(todos)
except Exception as e:
logger.warning(
f"Error getting todos from calendar {calendar['name']}: {e}"
"Error getting todos from calendar %s: %s", calendar["name"], e
)
continue
return all_todos
except Exception as e:
logger.error(f"Error searching todos across calendars: {e}")
logger.error("Error searching todos across calendars: %s", e)
raise
# ============= Helper Methods - Event iCalendar =============
@@ -935,7 +946,7 @@ class CalendarClient:
return self._extract_vevent_data(component)
return None
except Exception as e:
logger.error(f"Error parsing iCalendar event: {e}")
logger.error("Error parsing iCalendar event: %s", e)
return None
def _merge_ical_properties(
@@ -1060,7 +1071,7 @@ class CalendarClient:
return cal.to_ical().decode("utf-8")
except Exception as e:
logger.error(f"Error merging iCal properties: {e}")
logger.error("Error merging iCal properties: %s", e)
return self._create_ical_event(event_data, event_uid)
# ============= Helper Methods - Todo iCalendar =============
@@ -1184,7 +1195,7 @@ class CalendarClient:
return None
except Exception as e:
logger.error(f"Error parsing iCalendar todo: {e}")
logger.error("Error parsing iCalendar todo: %s", e)
return None
def _merge_ical_todo_properties(
@@ -1193,7 +1204,7 @@ class CalendarClient:
"""Merge new todo data into existing raw iCal while preserving all properties."""
try:
logger.debug(
f"Merging todo properties for {todo_uid}: {list(todo_data.keys())}"
"Merging todo properties for %s: %s", todo_uid, list(todo_data.keys())
)
cal = Calendar.from_ical(raw_ical)
@@ -1207,13 +1218,13 @@ class CalendarClient:
if "status" in todo_data:
status_value = todo_data["status"].upper()
component["STATUS"] = status_value
logger.debug(f"Set STATUS to {status_value}")
logger.debug("Set STATUS to %s", status_value)
if "priority" in todo_data:
component["PRIORITY"] = todo_data["priority"]
if "percent_complete" in todo_data:
percent_value = todo_data["percent_complete"]
component["PERCENT-COMPLETE"] = percent_value
logger.debug(f"Set PERCENT-COMPLETE to {percent_value}")
logger.debug("Set PERCENT-COMPLETE to %s", percent_value)
# Handle due date
if "due" in todo_data:
@@ -1221,7 +1232,7 @@ class CalendarClient:
if due_str:
due_dt = self._ensure_timezone_aware(due_str)
component["DUE"] = vDDDTypes(due_dt)
logger.debug(f"Set DUE to {due_dt}")
logger.debug("Set DUE to %s", due_dt)
# Handle start date
if "dtstart" in todo_data:
@@ -1229,7 +1240,7 @@ class CalendarClient:
if dtstart_str:
dtstart_dt = self._ensure_timezone_aware(dtstart_str)
component["DTSTART"] = vDDDTypes(dtstart_dt)
logger.debug(f"Set DTSTART to {dtstart_dt}")
logger.debug("Set DTSTART to %s", dtstart_dt)
# Handle completed date
if "completed" in todo_data:
@@ -1237,7 +1248,7 @@ class CalendarClient:
if completed_str:
completed_dt = self._ensure_timezone_aware(completed_str)
component["COMPLETED"] = vDDDTypes(completed_dt)
logger.debug(f"Set COMPLETED to {completed_dt}")
logger.debug("Set COMPLETED to %s", completed_dt)
# Handle categories
if "categories" in todo_data:
@@ -1246,7 +1257,7 @@ class CalendarClient:
component["CATEGORIES"] = [
c.strip() for c in categories_str.split(",")
]
logger.debug(f"Set CATEGORIES to {categories_str}")
logger.debug("Set CATEGORIES to %s", categories_str)
# Update timestamps
now = dt.datetime.now(dt.UTC)
@@ -1258,7 +1269,7 @@ class CalendarClient:
return cal.to_ical().decode("utf-8")
except Exception as e:
logger.error(f"Error merging iCal todo properties: {e}", exc_info=True)
logger.error("Error merging iCal todo properties: %s", e, exc_info=True)
return self._create_ical_todo(todo_data, todo_uid)
# ============= Helper Methods - Filtering =============
@@ -1290,7 +1301,7 @@ class CalendarClient:
return categories_obj.to_ical().decode("utf-8")
return str(categories_obj)
except Exception as e:
logger.warning(f"Error extracting categories: {e}")
logger.warning("Error extracting categories: %s", e)
return str(categories_obj)
def _apply_event_filters(
@@ -1437,7 +1448,7 @@ class CalendarClient:
}
except Exception as e:
logger.error(f"Error in bulk update: {e}")
logger.error("Error in bulk update: %s", e)
raise
async def find_availability(
+5 -5
View File
@@ -89,7 +89,7 @@ class ContactsClient(BaseNextcloudClient):
}
)
logger.debug(f"Found {len(addressbooks)} addressbooks")
logger.debug("Found %s addressbooks", len(addressbooks))
return addressbooks
async def create_addressbook(self, *, name: str, display_name: str):
@@ -166,7 +166,7 @@ class ContactsClient(BaseNextcloudClient):
except Exception:
# Fall back to creating new vCard if we can't get existing
logger.warning(
f"Could not fetch existing vCard for {uid}, creating new"
"Could not fetch existing vCard for %s, creating new", uid
)
raw_vcard_content = ""
@@ -283,7 +283,7 @@ class ContactsClient(BaseNextcloudClient):
}
)
logger.debug(f"Found {len(contacts)} contacts")
logger.debug("Found %s contacts", len(contacts))
return contacts
async def _get_raw_vcard(self, addressbook: str, uid: str) -> tuple[str, str]:
@@ -296,7 +296,7 @@ class ContactsClient(BaseNextcloudClient):
etag = response.headers.get("etag", "")
return response.text, etag
except Exception as e:
logger.error(f"Error getting raw vCard for {uid}: {e}")
logger.error("Error getting raw vCard for %s: %s", uid, e)
raise
def _merge_vcard_properties(
@@ -428,7 +428,7 @@ class ContactsClient(BaseNextcloudClient):
return "\n".join(updated_lines)
except Exception as e:
logger.error(f"Error merging vCard properties: {e}")
logger.error("Error merging vCard properties: %s", e)
# Fallback to creating basic vCard matching Nextcloud format
basic_vcard = f"""BEGIN:VCARD
VERSION:3.0
+1 -1
View File
@@ -130,7 +130,7 @@ class CookbookClient(BaseNextcloudClient):
Returns:
Full imported recipe data
"""
logger.info(f"Importing recipe from URL: {url}")
logger.info("Importing recipe from URL: %s", url)
response = await self._make_request(
"POST",
"/apps/cookbook/api/v1/import",
+3 -3
View File
@@ -67,7 +67,7 @@ class GroupsClient(BaseNextcloudClient):
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
)
response.raise_for_status()
logger.info(f"Created group: {groupid}")
logger.info("Created group: %s", groupid)
@retry_on_429
async def delete_group(self, groupid: str) -> None:
@@ -85,7 +85,7 @@ class GroupsClient(BaseNextcloudClient):
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
)
response.raise_for_status()
logger.info(f"Deleted group: {groupid}")
logger.info("Deleted group: %s", groupid)
@retry_on_429
async def get_group_members(self, groupid: str) -> List[str]:
@@ -150,4 +150,4 @@ class GroupsClient(BaseNextcloudClient):
headers={"OCS-APIRequest": "true", "Accept": "application/json"},
)
response.raise_for_status()
logger.info(f"Updated group {groupid} displayname to: {displayname}")
logger.info("Updated group %s displayname to: %s", groupid, displayname)
+26 -16
View File
@@ -93,13 +93,14 @@ class NotesClient(BaseNextcloudClient):
for note in response_data:
note_id = note.get("id")
if note_id is None:
logger.warning(f"Skipping note without ID: {note}")
logger.warning("Skipping note without ID: %s", note)
continue
# Skip duplicates (API returns all IDs in last chunk for deletion detection)
if note_id in seen_ids:
logger.debug(
f"Skipping duplicate note {note_id} (pruned version in last chunk)"
"Skipping duplicate note %s (pruned version in last chunk)",
note_id,
)
continue
@@ -152,10 +153,10 @@ class NotesClient(BaseNextcloudClient):
if category is not None:
old_note = await self.get_note(note_id)
old_category = old_note.get("category", "")
logger.info(f"Current category for note {note_id}: '{old_category}'")
logger.info("Current category for note %s: '%s'", note_id, old_category)
except Exception as e:
logger.warning(
f"Could not fetch current note {note_id} details before update: {e}"
"Could not fetch current note %s details before update: %s", note_id, e
)
old_note = None
@@ -169,7 +170,7 @@ class NotesClient(BaseNextcloudClient):
body["category"] = category
logger.info(
f"Attempting to update note {note_id} with etag {etag}. Body: {body}"
"Attempting to update note %s with etag %s. Body: %s", note_id, etag, body
)
response = await self._make_request(
@@ -180,7 +181,7 @@ class NotesClient(BaseNextcloudClient):
)
logger.info(
f"Update response for note {note_id}: Status {response.status_code}"
"Update response for note %s: Status %s", note_id, response.status_code
)
updated_note = _expect_note_object(response.json(), operation="update_note")
@@ -191,7 +192,9 @@ class NotesClient(BaseNextcloudClient):
and old_note.get("category", "") != category
):
logger.info(
f"Category changed from '{old_note.get('category', '')}' to '{category}' - cleaning up old attachment directory"
"Category changed from '%s' to '%s' - cleaning up old attachment directory",
old_note.get("category", ""),
category,
)
try:
webdav_client = WebDAVClient(self._client, self.username)
@@ -200,7 +203,9 @@ class NotesClient(BaseNextcloudClient):
)
except Exception as e:
logger.error(
f"Error cleaning up old attachment directory for note {note_id}: {e}"
"Error cleaning up old attachment directory for note %s: %s",
note_id,
e,
)
return updated_note
@@ -220,20 +225,23 @@ class NotesClient(BaseNextcloudClient):
potential_categories.append("") # Empty category
logger.info(
f"Note {note_id} has category: '{category}', will check attachment directories in: {potential_categories}"
"Note %s has category: '%s', will check attachment directories in: %s",
note_id,
category,
potential_categories,
)
except Exception as e:
logger.warning(
f"Could not fetch note {note_id} details before deletion: {e}"
"Could not fetch note %s details before deletion: %s", note_id, e
)
potential_categories = ["", "Unknown"] # Try common categories
# Delete the note via API
logger.info(f"Deleting note {note_id} via API")
logger.info("Deleting note %s via API", note_id)
response = await self._make_request(
"DELETE", f"/apps/notes/api/v1/notes/{note_id}"
)
logger.info(f"Note {note_id} deleted successfully via API")
logger.info("Note %s deleted successfully via API", note_id)
json_response = response.json()
# Clean up attachment directories
@@ -245,16 +253,16 @@ class NotesClient(BaseNextcloudClient):
await webdav_client.cleanup_note_attachments(note_id, cat)
except Exception as e:
logger.warning(
f"Failed to cleanup attachments for category '{cat}': {e}"
"Failed to cleanup attachments for category '%s': %s", cat, e
)
except Exception as e:
logger.warning(f"Error during attachment cleanup: {e}")
logger.warning("Error during attachment cleanup: %s", e)
return json_response
async def append_content(self, note_id: int, content: str) -> Dict[str, Any]:
"""Append content to an existing note with a separator."""
logger.info(f"Appending content to note {note_id}")
logger.info("Appending content to note %s", note_id)
# Get current note
current_note = await self.get_note(note_id)
@@ -270,7 +278,9 @@ class NotesClient(BaseNextcloudClient):
new_content = content # No separator needed for empty notes
logger.info(
f"Combining existing content ({len(existing_content)} chars) with new content ({len(content)} chars)"
"Combining existing content (%s chars) with new content (%s chars)",
len(existing_content),
len(content),
)
# Update with combined content
+8 -4
View File
@@ -72,8 +72,12 @@ class SharingClient(BaseNextcloudClient):
)
logger.info(
f"Created share {share_data['id']}: {path} -> {share_with} "
f"(type={share_type}, permissions={permissions})"
"Created share %s: %s -> %s (type=%s, permissions=%s)",
share_data["id"],
path,
share_with,
share_type,
permissions,
)
return share_data
@@ -99,7 +103,7 @@ class SharingClient(BaseNextcloudClient):
f"OCS API error: {data['ocs']['meta'].get('message', 'Unknown error')}"
)
logger.info(f"Deleted share {share_id}")
logger.info("Deleted share %s", share_id)
@retry_on_429
async def get_share(self, share_id: int) -> dict[str, Any]:
@@ -206,5 +210,5 @@ class SharingClient(BaseNextcloudClient):
f"OCS API error: {result['ocs']['meta'].get('message', 'Unknown error')}"
)
logger.info(f"Updated share {share_id}")
logger.info("Updated share %s", share_id)
return result["ocs"]["data"]
+110 -67
View File
@@ -28,7 +28,7 @@ class WebDAVClient(BaseNextcloudClient):
path_with_slash = path
webdav_path = f"{self._get_webdav_base_path()}/{path_with_slash.lstrip('/')}"
logger.debug(f"Deleting WebDAV resource: {webdav_path}")
logger.debug("Deleting WebDAV resource: %s", webdav_path)
headers = {"OCS-APIRequest": "true"}
try:
@@ -39,28 +39,30 @@ class WebDAVClient(BaseNextcloudClient):
"PROPFIND", webdav_path, headers=propfind_headers
)
logger.debug(
f"Resource exists check status: {propfind_resp.status_code}"
"Resource exists check status: %s", propfind_resp.status_code
)
except HTTPStatusError as e:
if e.response.status_code == 404:
logger.debug(f"Resource '{path}' doesn't exist, no deletion needed")
logger.debug(
"Resource '%s' doesn't exist, no deletion needed", path
)
return {"status_code": 404}
# For other errors, continue with deletion attempt
# Proceed with deletion
response = await self._make_request("DELETE", webdav_path, headers=headers)
logger.debug(f"Successfully deleted WebDAV resource '{path}'")
logger.debug("Successfully deleted WebDAV resource '%s'", path)
return {"status_code": response.status_code}
except HTTPStatusError as e:
if e.response.status_code == 404:
logger.debug(f"Resource '{path}' not found, no deletion needed")
logger.debug("Resource '%s' not found, no deletion needed", path)
return {"status_code": 404}
else:
logger.error(f"HTTP error deleting WebDAV resource '{path}': {e}")
logger.error("HTTP error deleting WebDAV resource '%s': %s", path, e)
raise e
except Exception as e:
logger.error(f"Unexpected error deleting WebDAV resource '{path}': {e}")
logger.error("Unexpected error deleting WebDAV resource '%s': %s", path, e)
raise e
async def cleanup_old_attachment_directory(
@@ -72,13 +74,15 @@ class WebDAVClient(BaseNextcloudClient):
f"Notes/{old_category_path_part}.attachments.{note_id}/"
)
logger.debug(f"Cleaning up old attachment directory: {old_attachment_dir_path}")
logger.debug(
"Cleaning up old attachment directory: %s", old_attachment_dir_path
)
try:
delete_result = await self.delete_resource(path=old_attachment_dir_path)
logger.debug(f"Cleanup result: {delete_result}")
logger.debug("Cleanup result: %s", delete_result)
return delete_result
except Exception as e:
logger.error(f"Error during cleanup of old attachment directory: {e}")
logger.error("Error during cleanup of old attachment directory: %s", e)
raise e
async def cleanup_note_attachments(
@@ -89,14 +93,14 @@ class WebDAVClient(BaseNextcloudClient):
attachment_dir_path = f"Notes/{cat_path_part}.attachments.{note_id}/"
logger.debug(
f"Cleaning up attachments for note {note_id} in category '{category}'"
"Cleaning up attachments for note %s in category '%s'", note_id, category
)
try:
delete_result = await self.delete_resource(path=attachment_dir_path)
logger.debug(f"Cleanup result for note {note_id}: {delete_result}")
logger.debug("Cleanup result for note %s: %s", note_id, delete_result)
return delete_result
except Exception as e:
logger.error(f"Failed cleaning up attachments for note {note_id}: {e}")
logger.error("Failed cleaning up attachments for note %s: %s", note_id, e)
raise e
async def add_note_attachment(
@@ -118,7 +122,7 @@ class WebDAVClient(BaseNextcloudClient):
parent_dir_path = f"{webdav_base}/{parent_dir_webdav_rel_path}"
attachment_path = f"{parent_dir_path}/{filename}"
logger.debug(f"Uploading attachment '{filename}' for note {note_id}")
logger.debug("Uploading attachment '%s' for note %s", filename, note_id)
if not mime_type:
mime_type, _ = mimetypes.guess_type(filename)
@@ -143,7 +147,8 @@ class WebDAVClient(BaseNextcloudClient):
)
elif notes_dir_response.status_code >= 400:
logger.error(
f"Error accessing WebDAV Notes directory: {notes_dir_response.status_code}"
"Error accessing WebDAV Notes directory: %s",
notes_dir_response.status_code,
)
notes_dir_response.raise_for_status()
@@ -156,7 +161,8 @@ class WebDAVClient(BaseNextcloudClient):
# MKCOL should return 201 Created or 405 Method Not Allowed (if directory already exists)
if mkcol_response.status_code not in [201, 405]:
logger.error(
f"Unexpected status code {mkcol_response.status_code} when creating attachments directory"
"Unexpected status code %s when creating attachments directory",
mkcol_response.status_code,
)
mkcol_response.raise_for_status()
@@ -166,18 +172,24 @@ class WebDAVClient(BaseNextcloudClient):
)
response.raise_for_status()
logger.debug(
f"Successfully uploaded attachment '{filename}' to note {note_id}"
"Successfully uploaded attachment '%s' to note %s", filename, note_id
)
return {"status_code": response.status_code}
except HTTPStatusError as e:
logger.error(
f"HTTP error uploading attachment '{filename}' to note {note_id}: {e}"
"HTTP error uploading attachment '%s' to note %s: %s",
filename,
note_id,
e,
)
raise e
except Exception as e:
logger.error(
f"Unexpected error uploading attachment '{filename}' to note {note_id}: {e}"
"Unexpected error uploading attachment '%s' to note %s: %s",
filename,
note_id,
e,
)
raise e
@@ -190,7 +202,7 @@ class WebDAVClient(BaseNextcloudClient):
attachment_dir_segment = f".attachments.{note_id}"
attachment_path = f"{webdav_base}/Notes/{category_path_part}{attachment_dir_segment}/{filename}"
logger.debug(f"Fetching attachment '{filename}' for note {note_id}")
logger.debug("Fetching attachment '%s' for note %s", filename, note_id)
try:
response = await self._make_request("GET", attachment_path)
@@ -200,21 +212,29 @@ class WebDAVClient(BaseNextcloudClient):
mime_type = response.headers.get("content-type", "application/octet-stream")
logger.debug(
f"Successfully fetched attachment '{filename}' ({len(content)} bytes)"
"Successfully fetched attachment '%s' (%s bytes)",
filename,
len(content),
)
return content, mime_type
except HTTPStatusError as e:
if e.response.status_code == 404:
logger.debug(f"Attachment '{filename}' not found for note {note_id}")
logger.debug("Attachment '%s' not found for note %s", filename, note_id)
else:
logger.error(
f"HTTP error fetching attachment '{filename}' for note {note_id}: {e}"
"HTTP error fetching attachment '%s' for note %s: %s",
filename,
note_id,
e,
)
raise e
except Exception as e:
logger.error(
f"Unexpected error fetching attachment '{filename}' for note {note_id}: {e}"
"Unexpected error fetching attachment '%s' for note %s: %s",
filename,
note_id,
e,
)
raise e
@@ -224,7 +244,7 @@ class WebDAVClient(BaseNextcloudClient):
if not webdav_path.endswith("/"):
webdav_path += "/"
logger.debug(f"Listing directory: {path}")
logger.debug("Listing directory: %s", path)
propfind_body = """<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:">
@@ -308,21 +328,21 @@ class WebDAVClient(BaseNextcloudClient):
}
)
logger.debug(f"Found {len(items)} items in directory: {path}")
logger.debug("Found %s items in directory: %s", len(items), path)
return items
except HTTPStatusError as e:
logger.error(f"HTTP error listing directory '{webdav_path}': {e}")
logger.error("HTTP error listing directory '%s': %s", webdav_path, e)
raise e
except Exception as e:
logger.error(f"Unexpected error listing directory '{webdav_path}': {e}")
logger.error("Unexpected error listing directory '%s': %s", webdav_path, e)
raise e
async def read_file(self, path: str) -> Tuple[bytes, str]:
"""Read a file's content via WebDAV GET."""
webdav_path = f"{self._get_webdav_base_path()}/{path.lstrip('/')}"
logger.debug(f"Reading file: {path}")
logger.debug("Reading file: %s", path)
try:
response = await self._make_request("GET", webdav_path)
@@ -333,14 +353,14 @@ class WebDAVClient(BaseNextcloudClient):
"content-type", "application/octet-stream"
)
logger.debug(f"Successfully read file '{path}' ({len(content)} bytes)")
logger.debug("Successfully read file '%s' (%s bytes)", path, len(content))
return content, content_type
except HTTPStatusError as e:
logger.error(f"HTTP error reading file '{path}': {e}")
logger.error("HTTP error reading file '%s': %s", path, e)
raise e
except Exception as e:
logger.error(f"Unexpected error reading file '{path}': {e}")
logger.error("Unexpected error reading file '%s': %s", path, e)
raise e
async def write_file(
@@ -349,7 +369,7 @@ class WebDAVClient(BaseNextcloudClient):
"""Write content to a file via WebDAV PUT."""
webdav_path = f"{self._get_webdav_base_path()}/{path.lstrip('/')}"
logger.debug(f"Writing file: {path}")
logger.debug("Writing file: %s", path)
if not content_type:
content_type, _ = mimetypes.guess_type(path)
@@ -364,14 +384,14 @@ class WebDAVClient(BaseNextcloudClient):
)
response.raise_for_status()
logger.debug(f"Successfully wrote file '{path}'")
logger.debug("Successfully wrote file '%s'", path)
return {"status_code": response.status_code}
except HTTPStatusError as e:
logger.error(f"HTTP error writing file '{path}': {e}")
logger.error("HTTP error writing file '%s': %s", path, e)
raise e
except Exception as e:
logger.error(f"Unexpected error writing file '{path}': {e}")
logger.error("Unexpected error writing file '%s': %s", path, e)
raise e
async def create_directory(
@@ -382,7 +402,7 @@ class WebDAVClient(BaseNextcloudClient):
if not webdav_path.endswith("/"):
webdav_path += "/"
logger.debug(f"Creating directory: {path}")
logger.debug("Creating directory: %s", path)
headers = {"OCS-APIRequest": "true"}
@@ -390,13 +410,13 @@ class WebDAVClient(BaseNextcloudClient):
response = await self._make_request("MKCOL", webdav_path, headers=headers)
response.raise_for_status()
logger.debug(f"Successfully created directory '{path}'")
logger.debug("Successfully created directory '%s'", path)
return {"status_code": response.status_code}
except HTTPStatusError as e:
# Method Not Allowed - directory already exists
if e.response.status_code == 405:
logger.debug(f"Directory '{path}' already exists")
logger.debug("Directory '%s' already exists", path)
return {"status_code": 405, "message": "Directory already exists"}
# File Conflict - parent directory does not exist
@@ -406,20 +426,21 @@ class WebDAVClient(BaseNextcloudClient):
if len(path_parts) > 1:
parent_dir = "/".join(path_parts[:-1])
logger.debug(
f"Parent directory '{parent_dir}' doesn't exist, creating recursively"
"Parent directory '%s' doesn't exist, creating recursively",
parent_dir,
)
await self.create_directory(parent_dir, recursive)
# Now try to create the original directory again
return await self.create_directory(path, recursive)
else:
# This shouldn't happen for single-level directories under root
logger.error(f"409 conflict for single-level directory '{path}'")
logger.error("409 conflict for single-level directory '%s'", path)
raise e
logger.error(f"HTTP error creating directory '{path}': {e}")
logger.error("HTTP error creating directory '%s': %s", path, e)
raise e
except Exception as e:
logger.error(f"Unexpected error creating directory '{path}': {e}")
logger.error("Unexpected error creating directory '%s': %s", path, e)
raise e
async def move_resource(
@@ -446,7 +467,7 @@ class WebDAVClient(BaseNextcloudClient):
elif not source_path.endswith("/") and destination_path.endswith("/"):
source_webdav_path += "/"
logger.debug(f"Moving resource from '{source_path}' to '{destination_path}'")
logger.debug("Moving resource from '%s' to '%s'", source_path, destination_path)
headers = {
"OCS-APIRequest": "true",
@@ -461,17 +482,20 @@ class WebDAVClient(BaseNextcloudClient):
response.raise_for_status()
logger.debug(
f"Successfully moved resource from '{source_path}' to '{destination_path}'"
"Successfully moved resource from '%s' to '%s'",
source_path,
destination_path,
)
return {"status_code": response.status_code}
except HTTPStatusError as e:
if e.response.status_code == 404:
logger.debug(f"Source resource '{source_path}' not found")
logger.debug("Source resource '%s' not found", source_path)
return {"status_code": 404, "message": "Source resource not found"}
elif e.response.status_code == 412:
logger.debug(
f"Destination '{destination_path}' already exists and overwrite is false"
"Destination '%s' already exists and overwrite is false",
destination_path,
)
return {
"status_code": 412,
@@ -479,7 +503,8 @@ class WebDAVClient(BaseNextcloudClient):
}
elif e.response.status_code == 409:
logger.debug(
f"Parent directory of destination '{destination_path}' doesn't exist"
"Parent directory of destination '%s' doesn't exist",
destination_path,
)
return {
"status_code": 409,
@@ -487,12 +512,18 @@ class WebDAVClient(BaseNextcloudClient):
}
else:
logger.error(
f"HTTP error moving resource from '{source_path}' to '{destination_path}': {e}"
"HTTP error moving resource from '%s' to '%s': %s",
source_path,
destination_path,
e,
)
raise e
except Exception as e:
logger.error(
f"Unexpected error moving resource from '{source_path}' to '{destination_path}': {e}"
"Unexpected error moving resource from '%s' to '%s': %s",
source_path,
destination_path,
e,
)
raise e
@@ -520,7 +551,9 @@ class WebDAVClient(BaseNextcloudClient):
elif not source_path.endswith("/") and destination_path.endswith("/"):
source_webdav_path += "/"
logger.debug(f"Copying resource from '{source_path}' to '{destination_path}'")
logger.debug(
"Copying resource from '%s' to '%s'", source_path, destination_path
)
headers = {
"OCS-APIRequest": "true",
@@ -535,17 +568,20 @@ class WebDAVClient(BaseNextcloudClient):
response.raise_for_status()
logger.debug(
f"Successfully copied resource from '{source_path}' to '{destination_path}'"
"Successfully copied resource from '%s' to '%s'",
source_path,
destination_path,
)
return {"status_code": response.status_code}
except HTTPStatusError as e:
if e.response.status_code == 404:
logger.debug(f"Source resource '{source_path}' not found")
logger.debug("Source resource '%s' not found", source_path)
return {"status_code": 404, "message": "Source resource not found"}
elif e.response.status_code == 412:
logger.debug(
f"Destination '{destination_path}' already exists and overwrite is false"
"Destination '%s' already exists and overwrite is false",
destination_path,
)
return {
"status_code": 412,
@@ -553,7 +589,8 @@ class WebDAVClient(BaseNextcloudClient):
}
elif e.response.status_code == 409:
logger.debug(
f"Parent directory of destination '{destination_path}' doesn't exist"
"Parent directory of destination '%s' doesn't exist",
destination_path,
)
return {
"status_code": 409,
@@ -561,12 +598,18 @@ class WebDAVClient(BaseNextcloudClient):
}
else:
logger.error(
f"HTTP error copying resource from '{source_path}' to '{destination_path}': {e}"
"HTTP error copying resource from '%s' to '%s': %s",
source_path,
destination_path,
e,
)
raise e
except Exception as e:
logger.error(
f"Unexpected error copying resource from '{source_path}' to '{destination_path}': {e}"
"Unexpected error copying resource from '%s' to '%s': %s",
source_path,
destination_path,
e,
)
raise e
@@ -615,7 +658,7 @@ class WebDAVClient(BaseNextcloudClient):
headers = {"Content-Type": "text/xml", "OCS-APIRequest": "true"}
logger.debug(f"Searching files in scope: {scope}")
logger.debug("Searching files in scope: %s", scope)
try:
response = await self._make_request(
@@ -626,14 +669,14 @@ class WebDAVClient(BaseNextcloudClient):
# Parse the XML response
results = self._parse_search_response(response.content, scope)
logger.debug(f"Search returned {len(results)} results")
logger.debug("Search returned %s results", len(results))
return results
except HTTPStatusError as e:
logger.error(f"HTTP error during search: {e}")
logger.error("HTTP error during search: %s", e)
raise e
except Exception as e:
logger.error(f"Unexpected error during search: {e}")
logger.error("Unexpected error during search: %s", e)
raise e
def _build_search_xml(
@@ -1129,7 +1172,7 @@ class WebDAVClient(BaseNextcloudClient):
"file_id": file_id,
}
logger.debug(f"Retrieved file info for ID {file_id}: {name}")
logger.debug("Retrieved file info for ID %s: %s", file_id, name)
return file_info
async def get_tag_by_name(self, tag_name: str) -> dict[str, Any] | None:
@@ -1449,7 +1492,7 @@ class WebDAVClient(BaseNextcloudClient):
"is_directory": is_directory,
}
logger.debug(f"Got file info for '{path}': id={file_info['id']}")
logger.debug("Got file info for '%s': id=%s", path, file_info["id"])
return file_info
async def create_tag(
@@ -1500,7 +1543,7 @@ class WebDAVClient(BaseNextcloudClient):
"userAssignable": user_assignable,
}
logger.info(f"Created tag '{name}' with ID {tag_info['id']}")
logger.info("Created tag '%s' with ID %s", name, tag_info["id"])
return tag_info
async def get_or_create_tag(
@@ -1522,7 +1565,7 @@ class WebDAVClient(BaseNextcloudClient):
# First try to get existing tag
existing_tag = await self.get_tag_by_name(name)
if existing_tag:
logger.debug(f"Tag '{name}' already exists with ID {existing_tag['id']}")
logger.debug("Tag '%s' already exists with ID %s", name, existing_tag["id"])
return existing_tag
# Create new tag
@@ -1558,7 +1601,7 @@ class WebDAVClient(BaseNextcloudClient):
# 201 = Created (new assignment), 409 = Conflict (already assigned)
if response.status_code in (201, 409):
logger.info(f"Tagged file {file_id} with tag {tag_id}")
logger.info("Tagged file %s with tag %s", file_id, tag_id)
return True
response.raise_for_status()
@@ -1584,7 +1627,7 @@ class WebDAVClient(BaseNextcloudClient):
# 204 = No Content (removed), 404 = Not Found (wasn't assigned)
if response.status_code in (204, 404):
logger.info(f"Removed tag {tag_id} from file {file_id}")
logger.info("Removed tag %s from file %s", tag_id, file_id)
return True
response.raise_for_status()