Skip to content

Commit 6637fa4

Browse files
✨(feature) add global async http client
1 parent d47f213 commit 6637fa4

File tree

12 files changed

+320
-149
lines changed

12 files changed

+320
-149
lines changed
Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,47 @@
1+
"""Docs service client for document management."""
2+
13
import logging
4+
from typing import Any
25

36
import httpx
4-
from bureaublad_api.exceptions import ExternalServiceError
57
from bureaublad_api.models.note import Note
68
from pydantic import TypeAdapter
79

810
logger = logging.getLogger(__name__)
911

1012

1113
class DocsClient:
12-
def __init__(self, base_url: str, token: str) -> None:
13-
self.base_url = base_url
14+
def __init__(self, http_client: httpx.AsyncClient, base_url: str, token: str) -> None:
15+
self.client = http_client
16+
self.base_url = base_url.rstrip("/")
1417
self.token = token
15-
self.client = httpx.Client(
16-
headers={"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
17-
)
1818

19-
def get_documents(
20-
self, path: str = "api/v1.0/documents/", page: int = 1, title: str | None = None, favorite: bool = False
19+
async def get_documents(
20+
self,
21+
path: str = "api/v1.0/documents/",
22+
page: int = 1,
23+
title: str | None = None,
24+
favorite: bool = False,
2125
) -> list[Note]:
22-
url = httpx.URL(f"{self.base_url}/{path}", params={"page": page})
26+
params: dict[str, Any] = {"page": page}
2327

2428
if title:
25-
url = url.copy_add_param("title", str(title))
29+
params["title"] = title
2630

2731
if favorite:
28-
url = url.copy_add_param("favorite", str(favorite))
32+
params["favorite"] = str(favorite)
33+
34+
url = f"{self.base_url}/{path.lstrip('/')}"
35+
response = await self.client.get(
36+
url,
37+
params=params,
38+
headers={"Authorization": f"Bearer {self.token}"},
39+
)
2940

30-
response = self.client.request("GET", url)
3141
if response.status_code != 200:
32-
logger.error(f"Docs get_documents failed: status={response.status_code}, url={url}")
33-
raise ExternalServiceError("Docs", f"Failed to fetch documents (status {response.status_code})")
42+
return TypeAdapter(list[Note]).validate_python([])
3443

3544
results = response.json().get("results", [])
36-
3745
notes: list[Note] = TypeAdapter(list[Note]).validate_python(results)
3846

3947
return notes
Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
1+
import logging
2+
from typing import Any
3+
14
import httpx
25
from bureaublad_api.models.document import Document
36
from pydantic import TypeAdapter
47

8+
logger = logging.getLogger(__name__)
9+
510

611
class DriveClient:
7-
def __init__(self, base_url: str, token: str) -> None:
8-
self.base_url = base_url
12+
def __init__(self, http_client: httpx.AsyncClient, base_url: str, token: str) -> None:
13+
self.client = http_client
14+
self.base_url = base_url.rstrip("/")
915
self.token = token
10-
self.client = httpx.Client(
11-
headers={"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
12-
)
1316

14-
def get_documents(
15-
self, path: str = "api/v1.0/items/", page: int = 1, title: str | None = None, favorite: bool = False
17+
async def get_documents(
18+
self,
19+
path: str = "api/v1.0/items/",
20+
page: int = 1,
21+
title: str | None = None,
22+
favorite: bool = False,
1623
) -> list[Document]:
17-
url = httpx.URL(f"{self.base_url}/{path}", params={"page": page})
24+
params: dict[str, Any] = {"page": page}
1825

1926
if title:
20-
url = url.copy_add_param("title", str(title))
27+
params["title"] = title
28+
29+
url = f"{self.base_url}/{path.lstrip('/')}"
30+
31+
response = await self.client.get(
32+
url,
33+
params=params,
34+
headers={"Authorization": f"Bearer {self.token}"},
35+
)
2136

22-
response = self.client.request("GET", url)
2337
if response.status_code != 200:
2438
return TypeAdapter(list[Document]).validate_python([])
2539

@@ -30,16 +44,17 @@ def get_documents(
3044

3145
workspace_id = results[0]["id"]
3246

33-
item_url = f"{self.base_url}/api/v1.0/items/{workspace_id}/children/"
47+
item_path = f"api/v1.0/items/{workspace_id}/children/"
48+
item_url = f"{self.base_url}/{item_path.lstrip('/')}"
49+
response = await self.client.get(
50+
item_url,
51+
headers={"Authorization": f"Bearer {self.token}"},
52+
)
3453

35-
response = self.client.request("GET", item_url)
3654
if response.status_code != 200:
3755
return TypeAdapter(list[Document]).validate_python([])
3856

3957
results = response.json().get("results", [])
58+
documents: list[Document] = TypeAdapter(list[Document]).validate_python(results)
4059

41-
print(results)
42-
43-
notes: list[Document] = TypeAdapter(list[Document]).validate_python(results)
44-
45-
return notes
60+
return documents
Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,68 @@
1+
"""Meet service client for meeting management."""
2+
3+
import logging
4+
from typing import Any
5+
16
import httpx
27
from bureaublad_api.models.meeting import Meeting
38
from pydantic import TypeAdapter
49

10+
logger = logging.getLogger(__name__)
11+
512

613
class MeetClient:
7-
def __init__(self, base_url: str, token: str) -> None:
8-
self.base_url = base_url
14+
"""Client for Meet service API.
15+
16+
Handles business logic for fetching and managing meetings.
17+
"""
18+
19+
def __init__(self, http_client: httpx.AsyncClient, base_url: str, token: str) -> None:
20+
"""Initialize MeetClient.
21+
22+
Args:
23+
http_client: Shared httpx.AsyncClient instance
24+
base_url: Base URL for the Meet service
25+
token: Authentication token for this request
26+
"""
27+
self.client = http_client
28+
self.base_url = base_url.rstrip("/")
929
self.token = token
10-
self.client = httpx.Client(
11-
headers={"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
12-
)
1330

14-
def get_meetings(
15-
self, path: str = "api/v1.0/documents/", page: int = 1, title: str | None = None, favorite: bool = False
31+
async def get_meetings(
32+
self,
33+
path: str = "api/v1.0/documents/",
34+
page: int = 1,
35+
title: str | None = None,
36+
favorite: bool = False,
1637
) -> list[Meeting]:
17-
url = httpx.URL(f"{self.base_url}/{path}", params={"page": page})
38+
"""Fetch meetings from Meet service.
1839
19-
if title:
20-
url = url.copy_add_param("title", str(title))
40+
Args:
41+
path: API endpoint path
42+
page: Page number for pagination
43+
title: Optional title filter
44+
favorite: Whether to filter for favorites only
2145
46+
Returns:
47+
List of Meeting objects
48+
"""
49+
params: dict[str, Any] = {"page": page}
50+
if title:
51+
params["title"] = title
2252
if favorite:
23-
url = url.copy_add_param("favorite", str(favorite))
53+
params["favorite"] = str(favorite)
54+
55+
url = f"{self.base_url}/{path.lstrip('/')}"
56+
response = await self.client.get(
57+
url,
58+
params=params,
59+
headers={"Authorization": f"Bearer {self.token}"},
60+
)
2461

25-
response = self.client.request("GET", url)
2662
if response.status_code != 200:
2763
return TypeAdapter(list[Meeting]).validate_python([])
2864

2965
results = response.json().get("results", [])
66+
meetings: list[Meeting] = TypeAdapter(list[Meeting]).validate_python(results)
3067

31-
notes: list[Meeting] = TypeAdapter(list[Meeting]).validate_python(results)
32-
33-
return notes
68+
return meetings

backend/bureaublad_api/clients/ocs.py

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
"""OCS (Open Collaboration Services) client for NextCloud API.
2+
3+
Reference: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/index.html
4+
"""
5+
16
import logging
27
from typing import cast
38

@@ -10,40 +15,53 @@
1015
logger = logging.getLogger(__name__)
1116

1217

13-
# https://docs.nextcloud.com/server/latest/developer_manual/client_apis/index.html
1418
class OCSClient:
15-
def __init__(self, base_url: str, token: str) -> None:
16-
self.base_url = base_url
19+
"""Client for NextCloud OCS API.
20+
21+
Handles business logic for activities, file search, calendar search, and task search.
22+
"""
23+
24+
def __init__(self, http_client: httpx.AsyncClient, base_url: str, token: str) -> None:
25+
"""Initialize OCSClient.
26+
27+
Args:
28+
http_client: Shared httpx.AsyncClient instance
29+
base_url: Base URL for the OCS service
30+
token: Authentication token for this request
31+
"""
32+
self.client = http_client
33+
self.base_url = base_url.rstrip("/")
1734
self.token = token
18-
self.client = httpx.Client(
19-
headers={
20-
"Authorization": f"Bearer {self.token}",
21-
"Content-Type": "application/json",
22-
"OCS-APIRequest": "true",
23-
"Accept": "application/json",
24-
}
25-
)
2635

27-
def get_activities(
36+
async def get_activities(
2837
self,
2938
path: str = "/ocs/v2.php/apps/activity/api/v2/activity",
3039
limit: int = 6,
3140
since: int = 0,
3241
filter: None | str = "files",
3342
) -> list[Activity]:
34-
url_string = f"{self.base_url}/{path}" if not filter else f"{self.base_url}/{path}/{filter}"
35-
36-
url = httpx.URL(url_string, params={"format": "json"})
43+
url_string = f"{path}/{filter}" if filter else path
3744

45+
params = {"format": "json"}
3846
if since:
39-
url = url.copy_add_param("since", str(since))
40-
47+
params["since"] = str(since)
4148
if limit:
42-
url = url.copy_add_param("limit", str(limit))
49+
params["limit"] = str(limit)
50+
51+
url = f"{self.base_url}/{url_string.lstrip('/')}"
52+
headers = {
53+
"Authorization": f"Bearer {self.token}",
54+
"OCS-APIRequest": "true",
55+
"Accept": "application/json",
56+
}
57+
response = await self.client.get(
58+
url,
59+
params=params,
60+
headers=headers,
61+
)
4362

44-
response = self.client.request("GET", url)
4563
if response.status_code != 200:
46-
logger.error(f"OCS activities request failed: status={response.status_code}, url={url}")
64+
logger.error(f"OCS activities request failed: status={response.status_code}, url={url_string}")
4765
raise ExternalServiceError("OCS", f"Failed to fetch activities (status {response.status_code})")
4866

4967
results = response.json().get("ocs", []).get("data", [])
@@ -52,10 +70,21 @@ def get_activities(
5270

5371
return notes
5472

55-
def search_files(self, term: str, path: str = "ocs/v2.php/search/providers/files/search") -> list[SearchResults]:
56-
url = httpx.URL(f"{self.base_url}/{path}", params={"term": term})
73+
async def search_files(
74+
self, term: str, path: str = "ocs/v2.php/search/providers/files/search"
75+
) -> list[SearchResults]:
76+
url = f"{self.base_url}/{path.lstrip('/')}"
77+
headers = {
78+
"Authorization": f"Bearer {self.token}",
79+
"OCS-APIRequest": "true",
80+
"Accept": "application/json",
81+
}
82+
response = await self.client.get(
83+
url,
84+
params={"term": term},
85+
headers=headers,
86+
)
5787

58-
response = self.client.request("GET", url)
5988
if response.status_code != 200:
6089
logger.error(f"OCS file search failed: status={response.status_code}, url={url}")
6190
raise ExternalServiceError("OCS", f"Failed to search files (status {response.status_code})")
@@ -66,12 +95,20 @@ def search_files(self, term: str, path: str = "ocs/v2.php/search/providers/files
6695
validated = TypeAdapter(list[FileSearchResult]).validate_python(results)
6796
return cast(list[SearchResults], validated)
6897

69-
def search_calendar(
98+
async def search_calendar(
7099
self, term: str, path: str = "ocs/v2.php/search/providers/calendar/search"
71100
) -> list[SearchResults]:
72-
url = httpx.URL(f"{self.base_url}/{path}", params={"term": term})
73-
74-
response = self.client.request("GET", url)
101+
url = f"{self.base_url}/{path.lstrip('/')}"
102+
headers = {
103+
"Authorization": f"Bearer {self.token}",
104+
"OCS-APIRequest": "true",
105+
"Accept": "application/json",
106+
}
107+
response = await self.client.get(
108+
url,
109+
params={"term": term},
110+
headers=headers,
111+
)
75112

76113
if response.status_code != 200:
77114
logger.error(f"OCS calendar search failed: status={response.status_code}, url={url}")
@@ -83,10 +120,21 @@ def search_calendar(
83120
validated = TypeAdapter(list[FileSearchResult]).validate_python(results)
84121
return cast(list[SearchResults], validated)
85122

86-
def search_tasks(self, term: str, path: str = "ocs/v2.php/search/providers/tasks/search") -> list[SearchResults]:
87-
url = httpx.URL(f"{self.base_url}/{path}", params={"term": term})
123+
async def search_tasks(
124+
self, term: str, path: str = "ocs/v2.php/search/providers/tasks/search"
125+
) -> list[SearchResults]:
126+
url = f"{self.base_url}/{path.lstrip('/')}"
127+
headers = {
128+
"Authorization": f"Bearer {self.token}",
129+
"OCS-APIRequest": "true",
130+
"Accept": "application/json",
131+
}
132+
response = await self.client.get(
133+
url,
134+
params={"term": term},
135+
headers=headers,
136+
)
88137

89-
response = self.client.request("GET", url)
90138
if response.status_code != 200:
91139
logger.error(f"OCS task search failed: status={response.status_code}, url={url}")
92140
raise ExternalServiceError("OCS", f"Failed to search tasks (status {response.status_code})")

0 commit comments

Comments
 (0)