magisterial 0.1.0__tar.gz

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,20 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - run: pip install -e . pytest anyio
20
+ - run: pytest -q
@@ -0,0 +1,24 @@
1
+ name: Publish to PyPI
2
+
3
+ # Publishes on GitHub release. Uses PyPI Trusted Publishing (OIDC) — configure
4
+ # this repo as a trusted publisher for the `magisterial` project on PyPI;
5
+ # no API token secret needed.
6
+
7
+ on:
8
+ release:
9
+ types: [published]
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ environment: pypi
15
+ permissions:
16
+ id-token: write
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+ - run: pip install build
23
+ - run: python -m build
24
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .venv/
8
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Magisterial
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: magisterial
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the Magisterial college sports data API
5
+ Project-URL: Homepage, https://magisterial.ai
6
+ Project-URL: Documentation, https://api.magisterial.ai/v1/docs
7
+ Project-URL: Repository, https://github.com/bluemens/magisterial-python
8
+ Project-URL: API reference (llms.txt), https://api.magisterial.ai/v1/llms.txt
9
+ Author-email: Magisterial <comfere@magisterial.ai>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,college sports,magisterial,ncaa,sdk,transfer portal
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx>=0.24.0
24
+ Requires-Dist: pydantic>=2.5.0
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Magisterial Python SDK
28
+
29
+ The official Python library for the [Magisterial](https://magisterial.ai) developer API —
30
+ college sports data across NCAA D1/D2/D3, NAIA, and NJCAA: players, teams, rosters,
31
+ cross-program careers, games, the live transfer portal, and an agent-backed
32
+ natural-language query endpoint.
33
+
34
+ - Interactive API reference: https://api.magisterial.ai/v1/docs
35
+ - OpenAPI spec: https://api.magisterial.ai/v1/openapi.json
36
+ - Agent-ready one-file reference: https://api.magisterial.ai/v1/llms.txt
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install magisterial
42
+ ```
43
+
44
+ Requires Python 3.10+.
45
+
46
+ ## Usage
47
+
48
+ Create an API key at [magisterial.ai/console/api-keys](https://magisterial.ai/console/api-keys)
49
+ and set `MAGISTERIAL_API_KEY` (or pass `api_key=` to the client).
50
+
51
+ ```python
52
+ from magisterial import Magisterial
53
+
54
+ client = Magisterial()
55
+
56
+ # Search players (auto-pagination follows the cursor for you)
57
+ page = client.players.search(
58
+ sport="soccer", division="D1", gender="women",
59
+ position="Forward", sort_by="goals",
60
+ )
61
+ for player in page.auto_paging_iter():
62
+ print(player.name, player.team, player.stats.get("goals"))
63
+
64
+ # One player's full profile
65
+ player = client.players.get(184223, sport="soccer", division="D3")
66
+
67
+ # Live transfer portal (usage-billed; use `since` for incremental polling)
68
+ portal = client.portal.list(sport="basketball", division="D1", status="INC")
69
+
70
+ # Natural-language query (usage-billed): submit and wait for the answer
71
+ run = client.query.create_and_poll(
72
+ prompt="Who led the NESCAC in assists this season?",
73
+ sport="soccer", division="D3", gender="men",
74
+ )
75
+ print(run.answer)
76
+ ```
77
+
78
+ ### Async
79
+
80
+ Every method is mirrored on `AsyncMagisterial`:
81
+
82
+ ```python
83
+ import asyncio
84
+ from magisterial import AsyncMagisterial
85
+
86
+ async def main():
87
+ async with AsyncMagisterial() as client:
88
+ page = await client.players.search(sport="soccer", division="D1")
89
+ async for player in page.auto_paging_iter():
90
+ print(player.name)
91
+
92
+ asyncio.run(main())
93
+ ```
94
+
95
+ ### Errors
96
+
97
+ Non-2xx responses raise typed exceptions carrying the API's error envelope:
98
+
99
+ ```python
100
+ from magisterial import Magisterial, NotFoundError, RateLimitError
101
+
102
+ client = Magisterial()
103
+ try:
104
+ client.players.get(1, sport="soccer", division="D1")
105
+ except NotFoundError as e:
106
+ print(e.error_code) # "player_not_found"
107
+ except RateLimitError as e:
108
+ print(e.retry_after) # seconds, from the Retry-After header
109
+ ```
110
+
111
+ `BillingError` (402) means API billing is not enabled or the monthly budget is
112
+ exhausted — manage both in the [developer console](https://magisterial.ai/console).
113
+
114
+ ### Retries
115
+
116
+ Idempotent requests (and `players.search`) are retried automatically on 429s,
117
+ 5xx and connection failures — up to `max_retries` (default 2), honoring the
118
+ server's `Retry-After`. Billable creates (`query.create`, `alerts.create`)
119
+ are never retried automatically.
120
+
121
+ ## Types
122
+
123
+ All request/response models live in `magisterial.types` and are generated from
124
+ the published OpenAPI spec (`scripts/sync-types.sh`), so they cannot drift
125
+ from the live API contract.
126
+
127
+ ## License
128
+
129
+ MIT
@@ -0,0 +1,103 @@
1
+ # Magisterial Python SDK
2
+
3
+ The official Python library for the [Magisterial](https://magisterial.ai) developer API —
4
+ college sports data across NCAA D1/D2/D3, NAIA, and NJCAA: players, teams, rosters,
5
+ cross-program careers, games, the live transfer portal, and an agent-backed
6
+ natural-language query endpoint.
7
+
8
+ - Interactive API reference: https://api.magisterial.ai/v1/docs
9
+ - OpenAPI spec: https://api.magisterial.ai/v1/openapi.json
10
+ - Agent-ready one-file reference: https://api.magisterial.ai/v1/llms.txt
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install magisterial
16
+ ```
17
+
18
+ Requires Python 3.10+.
19
+
20
+ ## Usage
21
+
22
+ Create an API key at [magisterial.ai/console/api-keys](https://magisterial.ai/console/api-keys)
23
+ and set `MAGISTERIAL_API_KEY` (or pass `api_key=` to the client).
24
+
25
+ ```python
26
+ from magisterial import Magisterial
27
+
28
+ client = Magisterial()
29
+
30
+ # Search players (auto-pagination follows the cursor for you)
31
+ page = client.players.search(
32
+ sport="soccer", division="D1", gender="women",
33
+ position="Forward", sort_by="goals",
34
+ )
35
+ for player in page.auto_paging_iter():
36
+ print(player.name, player.team, player.stats.get("goals"))
37
+
38
+ # One player's full profile
39
+ player = client.players.get(184223, sport="soccer", division="D3")
40
+
41
+ # Live transfer portal (usage-billed; use `since` for incremental polling)
42
+ portal = client.portal.list(sport="basketball", division="D1", status="INC")
43
+
44
+ # Natural-language query (usage-billed): submit and wait for the answer
45
+ run = client.query.create_and_poll(
46
+ prompt="Who led the NESCAC in assists this season?",
47
+ sport="soccer", division="D3", gender="men",
48
+ )
49
+ print(run.answer)
50
+ ```
51
+
52
+ ### Async
53
+
54
+ Every method is mirrored on `AsyncMagisterial`:
55
+
56
+ ```python
57
+ import asyncio
58
+ from magisterial import AsyncMagisterial
59
+
60
+ async def main():
61
+ async with AsyncMagisterial() as client:
62
+ page = await client.players.search(sport="soccer", division="D1")
63
+ async for player in page.auto_paging_iter():
64
+ print(player.name)
65
+
66
+ asyncio.run(main())
67
+ ```
68
+
69
+ ### Errors
70
+
71
+ Non-2xx responses raise typed exceptions carrying the API's error envelope:
72
+
73
+ ```python
74
+ from magisterial import Magisterial, NotFoundError, RateLimitError
75
+
76
+ client = Magisterial()
77
+ try:
78
+ client.players.get(1, sport="soccer", division="D1")
79
+ except NotFoundError as e:
80
+ print(e.error_code) # "player_not_found"
81
+ except RateLimitError as e:
82
+ print(e.retry_after) # seconds, from the Retry-After header
83
+ ```
84
+
85
+ `BillingError` (402) means API billing is not enabled or the monthly budget is
86
+ exhausted — manage both in the [developer console](https://magisterial.ai/console).
87
+
88
+ ### Retries
89
+
90
+ Idempotent requests (and `players.search`) are retried automatically on 429s,
91
+ 5xx and connection failures — up to `max_retries` (default 2), honoring the
92
+ server's `Retry-After`. Billable creates (`query.create`, `alerts.create`)
93
+ are never retried automatically.
94
+
95
+ ## Types
96
+
97
+ All request/response models live in `magisterial.types` and are generated from
98
+ the published OpenAPI spec (`scripts/sync-types.sh`), so they cannot drift
99
+ from the live API contract.
100
+
101
+ ## License
102
+
103
+ MIT
@@ -0,0 +1,44 @@
1
+ # Magisterial — official Python SDK for the Magisterial developer API.
2
+ # Docs: https://api.magisterial.ai/v1/docs Spec: https://api.magisterial.ai/v1/openapi.json
3
+
4
+ from . import types
5
+ from ._client import AsyncMagisterial, Magisterial
6
+ from ._exceptions import (
7
+ APIConnectionError,
8
+ APIStatusError,
9
+ APITimeoutError,
10
+ AuthenticationError,
11
+ BadRequestError,
12
+ BillingError,
13
+ InternalServerError,
14
+ MagisterialError,
15
+ NotFoundError,
16
+ PermissionDeniedError,
17
+ QueryPollTimeout,
18
+ RateLimitError,
19
+ UnprocessableEntityError,
20
+ )
21
+ from ._pagination import AsyncPage, SyncPage
22
+ from ._version import __version__
23
+
24
+ __all__ = [
25
+ "Magisterial",
26
+ "AsyncMagisterial",
27
+ "SyncPage",
28
+ "AsyncPage",
29
+ "types",
30
+ "MagisterialError",
31
+ "APIConnectionError",
32
+ "APITimeoutError",
33
+ "APIStatusError",
34
+ "BadRequestError",
35
+ "AuthenticationError",
36
+ "BillingError",
37
+ "PermissionDeniedError",
38
+ "NotFoundError",
39
+ "UnprocessableEntityError",
40
+ "RateLimitError",
41
+ "InternalServerError",
42
+ "QueryPollTimeout",
43
+ "__version__",
44
+ ]
@@ -0,0 +1,304 @@
1
+ # The Magisterial client core: auth, transport, retries, and JSON<->model
2
+ # plumbing shared by every resource namespace. Public entry points are
3
+ # ``Magisterial`` (sync) and ``AsyncMagisterial``.
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import random
9
+ from typing import Any, Dict, Mapping, Optional, Type, TypeVar
10
+
11
+ import httpx
12
+ from pydantic import BaseModel
13
+
14
+ from ._exceptions import (
15
+ APIConnectionError,
16
+ APIStatusError,
17
+ APITimeoutError,
18
+ MagisterialError,
19
+ error_from_response,
20
+ )
21
+ from ._version import __version__
22
+
23
+ M = TypeVar("M", bound=BaseModel)
24
+
25
+ DEFAULT_BASE_URL = "https://api.magisterial.ai"
26
+ DEFAULT_TIMEOUT = 30.0
27
+ DEFAULT_MAX_RETRIES = 2
28
+
29
+ # Methods safe to retry unconditionally. POSTs are retried only when the
30
+ # caller opts in (search is a read; query/alert creates are billable).
31
+ _IDEMPOTENT_METHODS = {"GET", "DELETE"}
32
+
33
+
34
+ def _clean_params(params: Optional[Mapping[str, Any]]) -> Dict[str, Any]:
35
+ return {k: v for k, v in (params or {}).items() if v is not None}
36
+
37
+
38
+ class _ClientConfig:
39
+ def __init__(
40
+ self,
41
+ api_key: Optional[str],
42
+ base_url: Optional[str],
43
+ timeout: float,
44
+ max_retries: int,
45
+ ) -> None:
46
+ self.api_key = api_key or os.environ.get("MAGISTERIAL_API_KEY")
47
+ if not self.api_key:
48
+ raise MagisterialError(
49
+ "No API key provided. Pass api_key=... or set the "
50
+ "MAGISTERIAL_API_KEY environment variable. Create keys at "
51
+ "https://magisterial.ai/console/api-keys"
52
+ )
53
+ self.base_url = (
54
+ base_url
55
+ or os.environ.get("MAGISTERIAL_BASE_URL")
56
+ or DEFAULT_BASE_URL
57
+ ).rstrip("/")
58
+ self.timeout = timeout
59
+ self.max_retries = max_retries
60
+
61
+ @property
62
+ def headers(self) -> Dict[str, str]:
63
+ return {
64
+ "Authorization": f"Bearer {self.api_key}",
65
+ "Accept": "application/json",
66
+ "User-Agent": f"magisterial-python/{__version__}",
67
+ }
68
+
69
+
70
+ def _retry_delay(response: Optional[httpx.Response], attempt: int) -> float:
71
+ """Seconds to sleep before retry `attempt` (0-based), honoring Retry-After."""
72
+ if response is not None:
73
+ header = response.headers.get("Retry-After")
74
+ if header:
75
+ try:
76
+ return max(0.0, float(header))
77
+ except ValueError:
78
+ pass
79
+ # Exponential backoff with jitter: ~0.5s, ~1s, ~2s ...
80
+ return 0.5 * (2**attempt) * (1 + random.random() * 0.25)
81
+
82
+
83
+ def _should_retry(response: httpx.Response) -> bool:
84
+ return response.status_code == 429 or response.status_code >= 500
85
+
86
+
87
+ class Magisterial:
88
+ """Synchronous client for the Magisterial developer API.
89
+
90
+ >>> client = Magisterial() # reads MAGISTERIAL_API_KEY
91
+ >>> page = client.players.search(sport="soccer", division="D1")
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ api_key: Optional[str] = None,
97
+ *,
98
+ base_url: Optional[str] = None,
99
+ timeout: float = DEFAULT_TIMEOUT,
100
+ max_retries: int = DEFAULT_MAX_RETRIES,
101
+ http_client: Optional[httpx.Client] = None,
102
+ ) -> None:
103
+ self._config = _ClientConfig(api_key, base_url, timeout, max_retries)
104
+ self._http = http_client or httpx.Client(timeout=timeout)
105
+
106
+ from .resources.alerts import Alerts
107
+ from .resources.games import Games
108
+ from .resources.persons import Persons
109
+ from .resources.players import Players
110
+ from .resources.portal import Portal
111
+ from .resources.query import Query
112
+ from .resources.reference import Reference
113
+ from .resources.teams import Teams
114
+
115
+ self.reference = Reference(self)
116
+ self.players = Players(self)
117
+ self.teams = Teams(self)
118
+ self.persons = Persons(self)
119
+ self.games = Games(self)
120
+ self.portal = Portal(self)
121
+ self.query = Query(self)
122
+ self.alerts = Alerts(self)
123
+
124
+ # -- transport ---------------------------------------------------------
125
+
126
+ def request(
127
+ self,
128
+ method: str,
129
+ path: str,
130
+ *,
131
+ params: Optional[Mapping[str, Any]] = None,
132
+ json: Optional[Any] = None,
133
+ retryable: Optional[bool] = None,
134
+ ) -> Any:
135
+ """Perform a request and return decoded JSON, retrying 429/5xx and
136
+ connection failures for idempotent (or explicitly retryable) calls."""
137
+ import time
138
+
139
+ url = self._config.base_url + path
140
+ can_retry = (
141
+ method in _IDEMPOTENT_METHODS if retryable is None else retryable
142
+ )
143
+ attempts = self._config.max_retries + 1 if can_retry else 1
144
+ last_exc: Optional[Exception] = None
145
+
146
+ for attempt in range(attempts):
147
+ try:
148
+ response = self._http.request(
149
+ method,
150
+ url,
151
+ params=_clean_params(params),
152
+ json=json,
153
+ headers=self._config.headers,
154
+ )
155
+ except httpx.TimeoutException as exc:
156
+ last_exc = APITimeoutError()
157
+ last_exc.__cause__ = exc
158
+ response = None
159
+ except httpx.HTTPError as exc:
160
+ last_exc = APIConnectionError(str(exc) or "Connection error.")
161
+ last_exc.__cause__ = exc
162
+ response = None
163
+
164
+ if response is not None:
165
+ if response.status_code < 400:
166
+ return response.json()
167
+ last_exc = error_from_response(response)
168
+ if not (attempt < attempts - 1 and _should_retry(response)):
169
+ raise last_exc
170
+ elif attempt >= attempts - 1:
171
+ raise last_exc # type: ignore[misc]
172
+
173
+ time.sleep(
174
+ _retry_delay(
175
+ getattr(last_exc, "response", None)
176
+ if isinstance(last_exc, APIStatusError)
177
+ else None,
178
+ attempt,
179
+ )
180
+ )
181
+
182
+ raise last_exc # type: ignore[misc] # unreachable, defensive
183
+
184
+ def get_model(self, path: str, model: Type[M], **params: Any) -> M:
185
+ return model.model_validate(self.request("GET", path, params=params))
186
+
187
+ def close(self) -> None:
188
+ self._http.close()
189
+
190
+ def __enter__(self) -> "Magisterial":
191
+ return self
192
+
193
+ def __exit__(self, *exc: Any) -> None:
194
+ self.close()
195
+
196
+
197
+ class AsyncMagisterial:
198
+ """Asynchronous client for the Magisterial developer API.
199
+
200
+ >>> client = AsyncMagisterial()
201
+ >>> page = await client.players.search(sport="soccer", division="D1")
202
+ """
203
+
204
+ def __init__(
205
+ self,
206
+ api_key: Optional[str] = None,
207
+ *,
208
+ base_url: Optional[str] = None,
209
+ timeout: float = DEFAULT_TIMEOUT,
210
+ max_retries: int = DEFAULT_MAX_RETRIES,
211
+ http_client: Optional[httpx.AsyncClient] = None,
212
+ ) -> None:
213
+ self._config = _ClientConfig(api_key, base_url, timeout, max_retries)
214
+ self._http = http_client or httpx.AsyncClient(timeout=timeout)
215
+
216
+ from .resources.alerts import AsyncAlerts
217
+ from .resources.games import AsyncGames
218
+ from .resources.persons import AsyncPersons
219
+ from .resources.players import AsyncPlayers
220
+ from .resources.portal import AsyncPortal
221
+ from .resources.query import AsyncQuery
222
+ from .resources.reference import AsyncReference
223
+ from .resources.teams import AsyncTeams
224
+
225
+ self.reference = AsyncReference(self)
226
+ self.players = AsyncPlayers(self)
227
+ self.teams = AsyncTeams(self)
228
+ self.persons = AsyncPersons(self)
229
+ self.games = AsyncGames(self)
230
+ self.portal = AsyncPortal(self)
231
+ self.query = AsyncQuery(self)
232
+ self.alerts = AsyncAlerts(self)
233
+
234
+ # -- transport ---------------------------------------------------------
235
+
236
+ async def request(
237
+ self,
238
+ method: str,
239
+ path: str,
240
+ *,
241
+ params: Optional[Mapping[str, Any]] = None,
242
+ json: Optional[Any] = None,
243
+ retryable: Optional[bool] = None,
244
+ ) -> Any:
245
+ import asyncio
246
+
247
+ url = self._config.base_url + path
248
+ can_retry = (
249
+ method in _IDEMPOTENT_METHODS if retryable is None else retryable
250
+ )
251
+ attempts = self._config.max_retries + 1 if can_retry else 1
252
+ last_exc: Optional[Exception] = None
253
+
254
+ for attempt in range(attempts):
255
+ try:
256
+ response = await self._http.request(
257
+ method,
258
+ url,
259
+ params=_clean_params(params),
260
+ json=json,
261
+ headers=self._config.headers,
262
+ )
263
+ except httpx.TimeoutException as exc:
264
+ last_exc = APITimeoutError()
265
+ last_exc.__cause__ = exc
266
+ response = None
267
+ except httpx.HTTPError as exc:
268
+ last_exc = APIConnectionError(str(exc) or "Connection error.")
269
+ last_exc.__cause__ = exc
270
+ response = None
271
+
272
+ if response is not None:
273
+ if response.status_code < 400:
274
+ return response.json()
275
+ last_exc = error_from_response(response)
276
+ if not (attempt < attempts - 1 and _should_retry(response)):
277
+ raise last_exc
278
+ elif attempt >= attempts - 1:
279
+ raise last_exc # type: ignore[misc]
280
+
281
+ await asyncio.sleep(
282
+ _retry_delay(
283
+ getattr(last_exc, "response", None)
284
+ if isinstance(last_exc, APIStatusError)
285
+ else None,
286
+ attempt,
287
+ )
288
+ )
289
+
290
+ raise last_exc # type: ignore[misc] # unreachable, defensive
291
+
292
+ async def get_model(self, path: str, model: Type[M], **params: Any) -> M:
293
+ return model.model_validate(
294
+ await self.request("GET", path, params=params)
295
+ )
296
+
297
+ async def close(self) -> None:
298
+ await self._http.aclose()
299
+
300
+ async def __aenter__(self) -> "AsyncMagisterial":
301
+ return self
302
+
303
+ async def __aexit__(self, *exc: Any) -> None:
304
+ await self.close()