datagsm-openapi-sdk 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ """DataGSM OpenAPI SDK for Python.
2
+
3
+ Official Python SDK for the DataGSM OpenAPI service.
4
+
5
+ Example:
6
+ Basic usage::
7
+
8
+ from datagsm_openapi import DataGsmClient
9
+
10
+ with DataGsmClient(api_key="your-api-key") as client:
11
+ students = client.students.get_students()
12
+ print(f"Total students: {students.total_elements}")
13
+ """
14
+
15
+ from .api import (
16
+ ClubApi,
17
+ ClubRequest,
18
+ MealRequest,
19
+ NeisApi,
20
+ ProjectApi,
21
+ ProjectRequest,
22
+ ScheduleRequest,
23
+ StudentApi,
24
+ StudentRequest,
25
+ )
26
+ from .client import DataGsmClient
27
+ from .exceptions import (
28
+ BadRequestException,
29
+ DataGsmException,
30
+ ForbiddenException,
31
+ NetworkException,
32
+ NotFoundException,
33
+ RateLimitException,
34
+ ServerErrorException,
35
+ UnauthorizedException,
36
+ ValidationException,
37
+ )
38
+
39
+ __version__ = "0.1.0"
40
+
41
+ __all__ = [
42
+ "BadRequestException",
43
+ "ClubApi",
44
+ "ClubRequest",
45
+ "DataGsmClient",
46
+ "DataGsmException",
47
+ "ForbiddenException",
48
+ "MealRequest",
49
+ "NeisApi",
50
+ "NetworkException",
51
+ "NotFoundException",
52
+ "ProjectApi",
53
+ "ProjectRequest",
54
+ "RateLimitException",
55
+ "ScheduleRequest",
56
+ "ServerErrorException",
57
+ "StudentApi",
58
+ "StudentRequest",
59
+ "UnauthorizedException",
60
+ "ValidationException",
61
+ ]
@@ -0,0 +1,291 @@
1
+ """HTTP client abstraction layer for DataGSM OpenAPI SDK."""
2
+
3
+ from typing import Any, Optional
4
+
5
+ import httpx
6
+
7
+ from .exceptions import (
8
+ BadRequestException,
9
+ DataGsmException,
10
+ ForbiddenException,
11
+ NetworkException,
12
+ NotFoundException,
13
+ RateLimitException,
14
+ ServerErrorException,
15
+ UnauthorizedException,
16
+ )
17
+
18
+
19
+ class HttpClient:
20
+ """HTTP client wrapper around httpx with error handling.
21
+
22
+ This class provides a clean interface for making HTTP requests to the DataGSM API
23
+ with automatic error handling and exception mapping.
24
+
25
+ Attributes:
26
+ base_url: Base URL for the API
27
+ api_key: API key for authentication
28
+ timeout: Request timeout in seconds
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ base_url: str,
34
+ api_key: str,
35
+ timeout: float = 30.0,
36
+ ) -> None:
37
+ """Initialize the HTTP client.
38
+
39
+ Args:
40
+ base_url: Base URL for the API
41
+ api_key: API key for authentication
42
+ timeout: Request timeout in seconds (default: 30.0)
43
+ """
44
+ self.base_url = base_url.rstrip("/")
45
+ self.api_key = api_key
46
+ self.timeout = timeout
47
+ self._client: Optional[httpx.Client] = None
48
+
49
+ def __enter__(self) -> "HttpClient":
50
+ """Enter context manager."""
51
+ self._client = httpx.Client(
52
+ base_url=self.base_url,
53
+ timeout=self.timeout,
54
+ headers=self._get_headers(),
55
+ )
56
+ return self
57
+
58
+ def __exit__(self, *args: Any) -> None:
59
+ """Exit context manager and close the client."""
60
+ self.close()
61
+
62
+ def _get_headers(self) -> dict[str, str]:
63
+ """Get common headers for all requests.
64
+
65
+ Returns:
66
+ Dictionary of HTTP headers
67
+ """
68
+ return {
69
+ "X-API-KEY": self.api_key,
70
+ "Accept": "application/json",
71
+ "User-Agent": "datagsm-openapi-sdk-python/0.1.0",
72
+ }
73
+
74
+ def _get_client(self) -> httpx.Client:
75
+ """Get or create the HTTP client.
76
+
77
+ Returns:
78
+ The httpx.Client instance
79
+ """
80
+ if self._client is None:
81
+ self._client = httpx.Client(
82
+ base_url=self.base_url,
83
+ timeout=self.timeout,
84
+ headers=self._get_headers(),
85
+ )
86
+ return self._client
87
+
88
+ def _handle_error(self, response: httpx.Response) -> None:
89
+ """Handle HTTP error responses.
90
+
91
+ Args:
92
+ response: The HTTP response object
93
+
94
+ Raises:
95
+ BadRequestException: For 400 errors
96
+ UnauthorizedException: For 401 errors
97
+ ForbiddenException: For 403 errors
98
+ NotFoundException: For 404 errors
99
+ RateLimitException: For 429 errors
100
+ ServerErrorException: For 5xx errors
101
+ DataGsmException: For other errors
102
+ """
103
+ try:
104
+ error_body = response.json()
105
+ error_message = error_body.get("message", response.text)
106
+ except Exception:
107
+ error_body = None
108
+ error_message = response.text or f"HTTP {response.status_code} error"
109
+
110
+ status_code = response.status_code
111
+
112
+ if status_code == 400:
113
+ raise BadRequestException(message=error_message, response_body=error_body)
114
+ elif status_code == 401:
115
+ raise UnauthorizedException(message=error_message, response_body=error_body)
116
+ elif status_code == 403:
117
+ raise ForbiddenException(message=error_message, response_body=error_body)
118
+ elif status_code == 404:
119
+ raise NotFoundException(message=error_message, response_body=error_body)
120
+ elif status_code == 429:
121
+ raise RateLimitException(message=error_message, response_body=error_body)
122
+ elif 500 <= status_code < 600:
123
+ raise ServerErrorException(
124
+ message=error_message,
125
+ status_code=status_code,
126
+ response_body=error_body,
127
+ )
128
+ else:
129
+ raise DataGsmException(
130
+ message=error_message,
131
+ status_code=status_code,
132
+ response_body=error_body,
133
+ )
134
+
135
+ def get(
136
+ self,
137
+ path: str,
138
+ params: Optional[dict[str, Any]] = None,
139
+ ) -> dict[str, Any]:
140
+ """Make a GET request.
141
+
142
+ Args:
143
+ path: API endpoint path (e.g., "/students")
144
+ params: Query parameters
145
+
146
+ Returns:
147
+ JSON response as a dictionary
148
+
149
+ Raises:
150
+ NetworkException: For connection or timeout errors
151
+ DataGsmException: For API errors
152
+ """
153
+ try:
154
+ client = self._get_client()
155
+ response = client.get(path, params=params)
156
+ response.raise_for_status()
157
+ return response.json() # type: ignore[no-any-return]
158
+ except httpx.HTTPStatusError as e:
159
+ self._handle_error(e.response)
160
+ raise # This line will never be reached, but makes mypy happy
161
+ except (httpx.ConnectError, httpx.TimeoutException) as e:
162
+ raise NetworkException(
163
+ message=f"Network error: {e!s}",
164
+ original_exception=e,
165
+ ) from e
166
+ except httpx.HTTPError as e:
167
+ raise NetworkException(
168
+ message=f"HTTP error: {e!s}",
169
+ original_exception=e,
170
+ ) from e
171
+
172
+ def post(
173
+ self,
174
+ path: str,
175
+ json: Optional[dict[str, Any]] = None,
176
+ params: Optional[dict[str, Any]] = None,
177
+ ) -> dict[str, Any]:
178
+ """Make a POST request.
179
+
180
+ Args:
181
+ path: API endpoint path
182
+ json: JSON request body
183
+ params: Query parameters
184
+
185
+ Returns:
186
+ JSON response as a dictionary
187
+
188
+ Raises:
189
+ NetworkException: For connection or timeout errors
190
+ DataGsmException: For API errors
191
+ """
192
+ try:
193
+ client = self._get_client()
194
+ response = client.post(path, json=json, params=params)
195
+ response.raise_for_status()
196
+ return response.json() # type: ignore[no-any-return]
197
+ except httpx.HTTPStatusError as e:
198
+ self._handle_error(e.response)
199
+ raise
200
+ except (httpx.ConnectError, httpx.TimeoutException) as e:
201
+ raise NetworkException(
202
+ message=f"Network error: {e!s}",
203
+ original_exception=e,
204
+ ) from e
205
+ except httpx.HTTPError as e:
206
+ raise NetworkException(
207
+ message=f"HTTP error: {e!s}",
208
+ original_exception=e,
209
+ ) from e
210
+
211
+ def put(
212
+ self,
213
+ path: str,
214
+ json: Optional[dict[str, Any]] = None,
215
+ params: Optional[dict[str, Any]] = None,
216
+ ) -> dict[str, Any]:
217
+ """Make a PUT request.
218
+
219
+ Args:
220
+ path: API endpoint path
221
+ json: JSON request body
222
+ params: Query parameters
223
+
224
+ Returns:
225
+ JSON response as a dictionary
226
+
227
+ Raises:
228
+ NetworkException: For connection or timeout errors
229
+ DataGsmException: For API errors
230
+ """
231
+ try:
232
+ client = self._get_client()
233
+ response = client.put(path, json=json, params=params)
234
+ response.raise_for_status()
235
+ return response.json() # type: ignore[no-any-return]
236
+ except httpx.HTTPStatusError as e:
237
+ self._handle_error(e.response)
238
+ raise
239
+ except (httpx.ConnectError, httpx.TimeoutException) as e:
240
+ raise NetworkException(
241
+ message=f"Network error: {e!s}",
242
+ original_exception=e,
243
+ ) from e
244
+ except httpx.HTTPError as e:
245
+ raise NetworkException(
246
+ message=f"HTTP error: {e!s}",
247
+ original_exception=e,
248
+ ) from e
249
+
250
+ def delete(
251
+ self,
252
+ path: str,
253
+ params: Optional[dict[str, Any]] = None,
254
+ ) -> dict[str, Any]:
255
+ """Make a DELETE request.
256
+
257
+ Args:
258
+ path: API endpoint path
259
+ params: Query parameters
260
+
261
+ Returns:
262
+ JSON response as a dictionary
263
+
264
+ Raises:
265
+ NetworkException: For connection or timeout errors
266
+ DataGsmException: For API errors
267
+ """
268
+ try:
269
+ client = self._get_client()
270
+ response = client.delete(path, params=params)
271
+ response.raise_for_status()
272
+ return response.json() # type: ignore[no-any-return]
273
+ except httpx.HTTPStatusError as e:
274
+ self._handle_error(e.response)
275
+ raise
276
+ except (httpx.ConnectError, httpx.TimeoutException) as e:
277
+ raise NetworkException(
278
+ message=f"Network error: {e!s}",
279
+ original_exception=e,
280
+ ) from e
281
+ except httpx.HTTPError as e:
282
+ raise NetworkException(
283
+ message=f"HTTP error: {e!s}",
284
+ original_exception=e,
285
+ ) from e
286
+
287
+ def close(self) -> None:
288
+ """Close the HTTP client and release resources."""
289
+ if self._client is not None:
290
+ self._client.close()
291
+ self._client = None
@@ -0,0 +1,53 @@
1
+ """JSON serialization utilities for DataGSM OpenAPI SDK."""
2
+
3
+ from datetime import date, datetime
4
+ from typing import Any
5
+
6
+
7
+ def serialize_date(value: date) -> str:
8
+ """Serialize a date to ISO format string.
9
+
10
+ Args:
11
+ value: Date object to serialize
12
+
13
+ Returns:
14
+ ISO format date string (YYYY-MM-DD)
15
+ """
16
+ return value.isoformat()
17
+
18
+
19
+ def serialize_datetime(value: datetime) -> str:
20
+ """Serialize a datetime to ISO format string.
21
+
22
+ Args:
23
+ value: Datetime object to serialize
24
+
25
+ Returns:
26
+ ISO format datetime string
27
+ """
28
+ return value.isoformat()
29
+
30
+
31
+ def clean_params(params: dict[str, Any]) -> dict[str, Any]:
32
+ """Clean query parameters by removing None values and serializing dates.
33
+
34
+ Args:
35
+ params: Dictionary of query parameters
36
+
37
+ Returns:
38
+ Cleaned dictionary with None values removed and dates serialized
39
+ """
40
+ cleaned: dict[str, Any] = {}
41
+ for key, value in params.items():
42
+ if value is None:
43
+ continue
44
+ if isinstance(value, datetime):
45
+ cleaned[key] = serialize_datetime(value)
46
+ elif isinstance(value, date):
47
+ cleaned[key] = serialize_date(value)
48
+ elif isinstance(value, bool):
49
+ # Convert boolean to lowercase string for query params
50
+ cleaned[key] = str(value).lower()
51
+ else:
52
+ cleaned[key] = value
53
+ return cleaned
@@ -0,0 +1,18 @@
1
+ """API modules for DataGSM OpenAPI SDK."""
2
+
3
+ from .club import ClubApi, ClubRequest
4
+ from .neis import MealRequest, NeisApi, ScheduleRequest
5
+ from .project import ProjectApi, ProjectRequest
6
+ from .student import StudentApi, StudentRequest
7
+
8
+ __all__ = [
9
+ "ClubApi",
10
+ "ClubRequest",
11
+ "MealRequest",
12
+ "NeisApi",
13
+ "ProjectApi",
14
+ "ProjectRequest",
15
+ "ScheduleRequest",
16
+ "StudentApi",
17
+ "StudentRequest",
18
+ ]
@@ -0,0 +1,73 @@
1
+ """Base API class for DataGSM OpenAPI SDK."""
2
+
3
+ from typing import Any, Optional, Type, TypeVar, cast
4
+
5
+ from pydantic import ValidationError
6
+
7
+ from .._http import HttpClient
8
+ from .._json import clean_params
9
+ from ..exceptions import ValidationException
10
+ from ..models import CommonApiResponse
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ class BaseApi:
16
+ """Base class for all API modules.
17
+
18
+ Provides common functionality for making HTTP requests and parsing responses.
19
+ """
20
+
21
+ def __init__(self, http_client: HttpClient) -> None:
22
+ """Initialize the base API.
23
+
24
+ Args:
25
+ http_client: HTTP client instance
26
+ """
27
+ self._http = http_client
28
+
29
+ def _get(
30
+ self,
31
+ path: str,
32
+ params: Optional[dict[str, Any]] = None,
33
+ response_type: Optional[Type[T]] = None,
34
+ ) -> T:
35
+ """Make a GET request and parse the response.
36
+
37
+ Args:
38
+ path: API endpoint path
39
+ params: Query parameters
40
+ response_type: Pydantic model class for response data
41
+
42
+ Returns:
43
+ Parsed response data
44
+
45
+ Raises:
46
+ ValidationException: If response validation fails
47
+ """
48
+ # Clean params (remove None values, serialize dates, etc.)
49
+ clean = clean_params(params) if params else None
50
+
51
+ # Make HTTP request
52
+ response_data = self._http.get(path, params=clean)
53
+
54
+ # If no response type specified, return raw data
55
+ if response_type is None:
56
+ return response_data # type: ignore[return-value]
57
+
58
+ # Parse as CommonApiResponse
59
+ try:
60
+ wrapped_response = CommonApiResponse[response_type].model_validate( # type: ignore[valid-type]
61
+ response_data
62
+ )
63
+ if wrapped_response.data is None:
64
+ raise ValidationException(
65
+ message="Response data is None",
66
+ validation_errors=[],
67
+ )
68
+ return cast(T, wrapped_response.data)
69
+ except ValidationError as e:
70
+ raise ValidationException(
71
+ message=f"Response validation failed: {e!s}",
72
+ validation_errors=cast(list[dict[str, Any]], e.errors()),
73
+ ) from e
@@ -0,0 +1,106 @@
1
+ """Club API module for DataGSM OpenAPI SDK."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ from ..models import ClubDetail, ClubResponse, ClubSortBy, ClubType, SortDirection
7
+ from ._base import BaseApi
8
+
9
+
10
+ @dataclass
11
+ class ClubRequest:
12
+ """동아리 조회 요청 파라미터 (Club Query Parameters).
13
+
14
+ Attributes:
15
+ club_id: Club ID for exact match
16
+ club_name: Club name for filtering
17
+ club_type: Club type filter (MAJOR_CLUB, JOB_CLUB, AUTONOMOUS_CLUB)
18
+ page: Page number (default: 0)
19
+ size: Page size (default: 100)
20
+ include_leader_in_participants: Include leader in participants list (default: False)
21
+ sort_by: Sort field
22
+ sort_direction: Sort direction (default: ASC)
23
+ """
24
+
25
+ club_id: Optional[int] = None
26
+ club_name: Optional[str] = None
27
+ club_type: Optional[ClubType] = None
28
+ page: int = 0
29
+ size: int = 100
30
+ include_leader_in_participants: bool = False
31
+ sort_by: Optional[ClubSortBy] = None
32
+ sort_direction: SortDirection = SortDirection.ASC
33
+
34
+ def to_params(self) -> dict[str, Optional[object]]:
35
+ """Convert to query parameters dictionary.
36
+
37
+ Returns:
38
+ Dictionary of query parameters
39
+ """
40
+ params: dict[str, Optional[object]] = {
41
+ "clubId": self.club_id,
42
+ "clubName": self.club_name,
43
+ "clubType": self.club_type.value if self.club_type else None,
44
+ "page": self.page,
45
+ "size": self.size,
46
+ "includeLeaderInParticipants": self.include_leader_in_participants,
47
+ "sortBy": self.sort_by.value if self.sort_by else None,
48
+ "sortDirection": self.sort_direction.value,
49
+ }
50
+ return params
51
+
52
+
53
+ class ClubApi(BaseApi):
54
+ """동아리 데이터 API (Club Data API).
55
+
56
+ Provides methods for querying club information.
57
+ """
58
+
59
+ def get_clubs(self, request: Optional[ClubRequest] = None) -> ClubResponse:
60
+ """동아리 목록 조회 (Get Club List).
61
+
62
+ Query clubs with optional filtering, sorting, and pagination.
63
+
64
+ Args:
65
+ request: Query parameters (optional)
66
+
67
+ Returns:
68
+ Paginated club response
69
+
70
+ Example:
71
+ >>> api = ClubApi(http_client)
72
+ >>> # Get all clubs
73
+ >>> response = api.get_clubs()
74
+ >>> print(f"Total: {response.total_elements}")
75
+ >>>
76
+ >>> # Filter by type
77
+ >>> request = ClubRequest(club_type=ClubType.MAJOR_CLUB)
78
+ >>> major_clubs = api.get_clubs(request)
79
+ """
80
+ req = request or ClubRequest()
81
+ return self._get("/v1/clubs", params=req.to_params(), response_type=ClubResponse)
82
+
83
+ def get_club(self, club_id: int) -> Optional[ClubDetail]:
84
+ """특정 동아리 조회 (Get Specific Club).
85
+
86
+ Retrieve a single club by ID with detailed information.
87
+
88
+ Args:
89
+ club_id: Club ID
90
+
91
+ Returns:
92
+ Club detail information if found, None otherwise
93
+
94
+ Example:
95
+ >>> api = ClubApi(http_client)
96
+ >>> club = api.get_club(1)
97
+ >>> if club:
98
+ ... print(f"Club: {club.name}")
99
+ ... print(f"Leader: {club.leader.name}")
100
+ """
101
+ request = ClubRequest(club_id=club_id)
102
+ response = self.get_clubs(request)
103
+
104
+ if response.clubs:
105
+ return response.clubs[0]
106
+ return None