From 650929c72df8912e7b207988ffbd808f4e3df60f Mon Sep 17 00:00:00 2001 From: fern-api <115122769+fern-api[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 05:34:41 +0000 Subject: [PATCH] SDK regeneration --- .github/workflows/ci.yml | 29 +- pyproject.toml | 9 +- src/codecombat/__init__.py | 62 +- src/codecombat/client.py | 187 +++-- src/codecombat/core/__init__.py | 13 +- src/codecombat/core/api_error.py | 3 + src/codecombat/core/client_wrapper.py | 68 ++ src/codecombat/core/jsonable_encoder.py | 7 + src/codecombat/core/remove_none_from_dict.py | 11 + .../core/remove_none_from_headers.py | 11 - src/codecombat/environment.py | 2 +- src/codecombat/resources/__init__.py | 50 +- src/codecombat/resources/auth/client.py | 98 ++- src/codecombat/resources/clans/__init__.py | 3 - src/codecombat/resources/clans/client.py | 70 +- .../resources/clans/types/__init__.py | 5 - .../resources/classrooms/__init__.py | 3 - src/codecombat/resources/classrooms/client.py | 466 +++++++++---- .../resources/classrooms/types/__init__.py | 10 - src/codecombat/resources/commons/__init__.py | 29 - .../resources/commons/types/__init__.py | 27 - src/codecombat/resources/stats/__init__.py | 3 - src/codecombat/resources/stats/client.py | 92 ++- .../resources/stats/types/__init__.py | 6 - src/codecombat/resources/users/__init__.py | 3 - src/codecombat/resources/users/client.py | 651 +++++++++++------- .../resources/users/types/__init__.py | 6 - src/codecombat/types/__init__.py | 51 ++ .../clans => }/types/clan_response.py | 5 +- .../commons => }/types/classroom_response.py | 17 +- .../classroom_response_courses_item.py} | 5 +- .../types/classroom_response_with_code.py | 7 +- ...assroom_response_with_code_courses_item.py | 30 + .../classrooms_create_request_ace_config.py} | 7 +- ...srooms_get_members_stats_response_item.py} | 11 +- ..._get_members_stats_response_item_stats.py} | 7 +- .../commons => }/types/datetime_string.py | 0 .../types/level_session_response.py | 23 +- .../level_session_response_level.py} | 7 +- .../level_session_response_state.py} | 5 +- .../types/license_stats_response.py | 11 +- .../commons => }/types/object_id_string.py | 0 .../types/playtime_stats_response.py | 9 +- .../commons => }/types/role_string.py | 0 .../commons => }/types/user_response.py | 21 +- .../user_response_license.py} | 5 +- .../user_response_o_auth_identities_item.py} | 5 +- .../user_response_stats.py} | 7 +- .../user_response_subscription.py} | 5 +- .../users_create_request_hero_config.py} | 7 +- .../users_create_request_role.py} | 8 +- tests/__init__.py | 0 tests/test_client.py | 6 + 53 files changed, 1387 insertions(+), 796 deletions(-) create mode 100644 src/codecombat/core/client_wrapper.py create mode 100644 src/codecombat/core/remove_none_from_dict.py delete mode 100644 src/codecombat/core/remove_none_from_headers.py delete mode 100644 src/codecombat/resources/clans/types/__init__.py delete mode 100644 src/codecombat/resources/classrooms/types/__init__.py delete mode 100644 src/codecombat/resources/commons/__init__.py delete mode 100644 src/codecombat/resources/commons/types/__init__.py delete mode 100644 src/codecombat/resources/stats/types/__init__.py delete mode 100644 src/codecombat/resources/users/types/__init__.py create mode 100644 src/codecombat/types/__init__.py rename src/codecombat/{resources/clans => }/types/clan_response.py (90%) rename src/codecombat/{resources/commons => }/types/classroom_response.py (63%) rename src/codecombat/{resources/commons/types/course.py => types/classroom_response_courses_item.py} (87%) rename src/codecombat/{resources/commons => }/types/classroom_response_with_code.py (83%) create mode 100644 src/codecombat/types/classroom_response_with_code_courses_item.py rename src/codecombat/{resources/classrooms/types/ace_config.py => types/classrooms_create_request_ace_config.py} (79%) rename src/codecombat/{resources/classrooms/types/member_stat.py => types/classrooms_get_members_stats_response_item.py} (67%) rename src/codecombat/{resources/classrooms/types/play_stats.py => types/classrooms_get_members_stats_response_item_stats.py} (82%) rename src/codecombat/{resources/commons => }/types/datetime_string.py (100%) rename src/codecombat/{resources/classrooms => }/types/level_session_response.py (64%) rename src/codecombat/{resources/classrooms/types/level.py => types/level_session_response_level.py} (81%) rename src/codecombat/{resources/classrooms/types/state.py => types/level_session_response_state.py} (83%) rename src/codecombat/{resources/stats => }/types/license_stats_response.py (71%) rename src/codecombat/{resources/commons => }/types/object_id_string.py (100%) rename src/codecombat/{resources/stats => }/types/playtime_stats_response.py (76%) rename src/codecombat/{resources/commons => }/types/role_string.py (100%) rename src/codecombat/{resources/commons => }/types/user_response.py (62%) rename src/codecombat/{resources/commons/types/license.py => types/user_response_license.py} (85%) rename src/codecombat/{resources/commons/types/auth_identity.py => types/user_response_o_auth_identities_item.py} (83%) rename src/codecombat/{resources/commons/types/user_stats.py => types/user_response_stats.py} (79%) rename src/codecombat/{resources/commons/types/subscription.py => types/user_response_subscription.py} (85%) rename src/codecombat/{resources/users/types/hero_config.py => types/users_create_request_hero_config.py} (80%) rename src/codecombat/{resources/users/types/user_role.py => types/users_create_request_role.py} (60%) create mode 100644 tests/__init__.py create mode 100644 tests/test_client.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2c1727..0a5b56e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: ci on: [push] jobs: compile: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout repo uses: actions/checkout@v3 @@ -13,17 +13,32 @@ jobs: python-version: 3.7 - name: Bootstrap poetry run: | - curl -sSL https://install.python-poetry.org | python - -y + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 - name: Install dependencies run: poetry install - name: Compile run: poetry run mypy . + test: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.7 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Test + run: poetry run pytest . publish: - needs: [ compile ] + needs: [compile, test] if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - runs-on: ubuntu-latest - + runs-on: ubuntu-20.04 steps: - name: Checkout repo uses: actions/checkout@v3 @@ -33,7 +48,7 @@ jobs: python-version: 3.7 - name: Bootstrap poetry run: | - curl -sSL https://install.python-poetry.org | python - -y + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 - name: Install dependencies run: poetry install - name: Publish to pypi @@ -42,4 +57,4 @@ jobs: poetry --no-interaction -v publish --build --repository remote --username "$PYPI_USERNAME" --password "$PYPI_PASSWORD" env: PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} - PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} \ No newline at end of file + PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} diff --git a/pyproject.toml b/pyproject.toml index a2a4265..0767283 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,8 @@ - [tool.poetry] name = "codecombat" -version = "0.0.18" +version = "0.1.6" description = "" +readme = "README.md" authors = [] packages = [ { include = "codecombat", from = "src"} @@ -10,13 +10,12 @@ packages = [ [tool.poetry.dependencies] python = "^3.7" +httpx = ">=0.21.2" pydantic = "^1.9.2" -httpx = "0.23.3" -types-backports = "0.1.3" -backports-cached_property = "1.0.2" [tool.poetry.dev-dependencies] mypy = "0.971" +pytest = "^7.4.0" [build-system] requires = ["poetry-core"] diff --git a/src/codecombat/__init__.py b/src/codecombat/__init__.py index 35cd442..ef7f6c5 100644 --- a/src/codecombat/__init__.py +++ b/src/codecombat/__init__.py @@ -1,65 +1,61 @@ # This file was auto-generated by Fern from our API Definition. -from .environment import CodeCombatEnvironment -from .resources import ( - AceConfig, - AuthIdentity, +from .types import ( ClanResponse, ClassroomResponse, + ClassroomResponseCoursesItem, ClassroomResponseWithCode, - Course, + ClassroomResponseWithCodeCoursesItem, + ClassroomsCreateRequestAceConfig, + ClassroomsGetMembersStatsResponseItem, + ClassroomsGetMembersStatsResponseItemStats, DatetimeString, - HeroConfig, - Level, LevelSessionResponse, - License, + LevelSessionResponseLevel, + LevelSessionResponseState, LicenseStatsResponse, - MemberStat, ObjectIdString, - PlayStats, PlaytimeStatsResponse, RoleString, - State, - Subscription, UserResponse, - UserRole, - UserStats, - auth, - clans, - classrooms, - commons, - stats, - users, + UserResponseLicense, + UserResponseOAuthIdentitiesItem, + UserResponseStats, + UserResponseSubscription, + UsersCreateRequestHeroConfig, + UsersCreateRequestRole, ) +from .resources import auth, clans, classrooms, stats, users +from .environment import CodeCombatEnvironment __all__ = [ - "AceConfig", - "AuthIdentity", "ClanResponse", "ClassroomResponse", + "ClassroomResponseCoursesItem", "ClassroomResponseWithCode", + "ClassroomResponseWithCodeCoursesItem", + "ClassroomsCreateRequestAceConfig", + "ClassroomsGetMembersStatsResponseItem", + "ClassroomsGetMembersStatsResponseItemStats", "CodeCombatEnvironment", - "Course", "DatetimeString", - "HeroConfig", - "Level", "LevelSessionResponse", - "License", + "LevelSessionResponseLevel", + "LevelSessionResponseState", "LicenseStatsResponse", - "MemberStat", "ObjectIdString", - "PlayStats", "PlaytimeStatsResponse", "RoleString", - "State", - "Subscription", "UserResponse", - "UserRole", - "UserStats", + "UserResponseLicense", + "UserResponseOAuthIdentitiesItem", + "UserResponseStats", + "UserResponseSubscription", + "UsersCreateRequestHeroConfig", + "UsersCreateRequestRole", "auth", "clans", "classrooms", - "commons", "stats", "users", ] diff --git a/src/codecombat/client.py b/src/codecombat/client.py index bc24ecf..3e934b7 100644 --- a/src/codecombat/client.py +++ b/src/codecombat/client.py @@ -1,68 +1,157 @@ # This file was auto-generated by Fern from our API Definition. -from backports.cached_property import cached_property +import typing +import urllib.parse +from json.decoder import JSONDecodeError +import httpx +import pydantic + +from .core.api_error import ApiError +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .core.jsonable_encoder import jsonable_encoder from .environment import CodeCombatEnvironment from .resources.auth.client import AsyncAuthClient, AuthClient from .resources.clans.client import AsyncClansClient, ClansClient from .resources.classrooms.client import AsyncClassroomsClient, ClassroomsClient from .resources.stats.client import AsyncStatsClient, StatsClient from .resources.users.client import AsyncUsersClient, UsersClient +from .types.user_response import UserResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) class CodeCombat: def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str + self, + *, + base_url: typing.Optional[str] = None, + environment: CodeCombatEnvironment = CodeCombatEnvironment.DEFAULT, + username: typing.Union[str, typing.Callable[[], str]], + password: typing.Union[str, typing.Callable[[], str]], + timeout: typing.Optional[float] = 60, ): - self._environment = environment - self._username = username - self._password = password - - @cached_property - def auth(self) -> AuthClient: - return AuthClient(environment=self._environment, username=self._username, password=self._password) - - @cached_property - def clans(self) -> ClansClient: - return ClansClient(environment=self._environment, username=self._username, password=self._password) - - @cached_property - def classrooms(self) -> ClassroomsClient: - return ClassroomsClient(environment=self._environment, username=self._username, password=self._password) - - @cached_property - def stats(self) -> StatsClient: - return StatsClient(environment=self._environment, username=self._username, password=self._password) - - @cached_property - def users(self) -> UsersClient: - return UsersClient(environment=self._environment, username=self._username, password=self._password) + self._client_wrapper = SyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + username=username, + password=password, + httpx_client=httpx.Client(timeout=timeout), + ) + self.auth = AuthClient(client_wrapper=self._client_wrapper) + self.clans = ClansClient(client_wrapper=self._client_wrapper) + self.classrooms = ClassroomsClient(client_wrapper=self._client_wrapper) + self.stats = StatsClient(client_wrapper=self._client_wrapper) + self.users = UsersClient(client_wrapper=self._client_wrapper) + + def post_users_handle_o_auth_identities( + self, + handle: str, + *, + provider: str, + access_token: typing.Optional[str] = OMIT, + code: typing.Optional[str] = OMIT, + ) -> UserResponse: + """ + Adds an OAuth2 identity to the user, so that they can be logged in with that identity. You need to send the OAuth code or the access token to this endpoint. 1. If no access token is provided, it will use your OAuth2 token URL to exchange the given code for an access token. 2. Then it will use the access token (given by you, or received from step 1) to look up the user on your service using the lookup URL, and expects a JSON object in response with an `id` property. 3. It will then save that user `id` to the user in our db as a new OAuthIdentity. In this example, we call your lookup URL (let's say, `https://oauth.provider/user?t=<%= accessToken %>`) with the access token (`1234`). The lookup URL returns `{ id: 'abcd' }` in this case, which we save to the user in our db. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - provider: str. Your OAuth Provider ID. + + - access_token: typing.Optional[str]. Will be passed through your lookup URL to get the user ID. Required if no `code`. + + - code: typing.Optional[str]. Will be passed to the OAuth token endpoint to get a token. Required if no `accessToken`. + """ + _request: typing.Dict[str, typing.Any] = {"provider": provider} + if access_token is not OMIT: + _request["accessToken"] = access_token + if code is not OMIT: + _request["code"] = code + _response = self._client_wrapper.httpx_client.request( + "POST", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/o-auth-identities"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) + if 200 <= _response.status_code < 300: + return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) class AsyncCodeCombat: def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str + self, + *, + base_url: typing.Optional[str] = None, + environment: CodeCombatEnvironment = CodeCombatEnvironment.DEFAULT, + username: typing.Union[str, typing.Callable[[], str]], + password: typing.Union[str, typing.Callable[[], str]], + timeout: typing.Optional[float] = 60, ): - self._environment = environment - self._username = username - self._password = password - - @cached_property - def auth(self) -> AsyncAuthClient: - return AsyncAuthClient(environment=self._environment, username=self._username, password=self._password) - - @cached_property - def clans(self) -> AsyncClansClient: - return AsyncClansClient(environment=self._environment, username=self._username, password=self._password) - - @cached_property - def classrooms(self) -> AsyncClassroomsClient: - return AsyncClassroomsClient(environment=self._environment, username=self._username, password=self._password) - - @cached_property - def stats(self) -> AsyncStatsClient: - return AsyncStatsClient(environment=self._environment, username=self._username, password=self._password) - - @cached_property - def users(self) -> AsyncUsersClient: - return AsyncUsersClient(environment=self._environment, username=self._username, password=self._password) + self._client_wrapper = AsyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + username=username, + password=password, + httpx_client=httpx.AsyncClient(timeout=timeout), + ) + self.auth = AsyncAuthClient(client_wrapper=self._client_wrapper) + self.clans = AsyncClansClient(client_wrapper=self._client_wrapper) + self.classrooms = AsyncClassroomsClient(client_wrapper=self._client_wrapper) + self.stats = AsyncStatsClient(client_wrapper=self._client_wrapper) + self.users = AsyncUsersClient(client_wrapper=self._client_wrapper) + + async def post_users_handle_o_auth_identities( + self, + handle: str, + *, + provider: str, + access_token: typing.Optional[str] = OMIT, + code: typing.Optional[str] = OMIT, + ) -> UserResponse: + """ + Adds an OAuth2 identity to the user, so that they can be logged in with that identity. You need to send the OAuth code or the access token to this endpoint. 1. If no access token is provided, it will use your OAuth2 token URL to exchange the given code for an access token. 2. Then it will use the access token (given by you, or received from step 1) to look up the user on your service using the lookup URL, and expects a JSON object in response with an `id` property. 3. It will then save that user `id` to the user in our db as a new OAuthIdentity. In this example, we call your lookup URL (let's say, `https://oauth.provider/user?t=<%= accessToken %>`) with the access token (`1234`). The lookup URL returns `{ id: 'abcd' }` in this case, which we save to the user in our db. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - provider: str. Your OAuth Provider ID. + + - access_token: typing.Optional[str]. Will be passed through your lookup URL to get the user ID. Required if no `code`. + + - code: typing.Optional[str]. Will be passed to the OAuth token endpoint to get a token. Required if no `accessToken`. + """ + _request: typing.Dict[str, typing.Any] = {"provider": provider} + if access_token is not OMIT: + _request["accessToken"] = access_token + if code is not OMIT: + _request["code"] = code + _response = await self._client_wrapper.httpx_client.request( + "POST", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/o-auth-identities"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) + if 200 <= _response.status_code < 300: + return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + +def _get_base_url(*, base_url: typing.Optional[str] = None, environment: CodeCombatEnvironment) -> str: + if base_url is not None: + return base_url + elif environment is not None: + return environment.value + else: + raise Exception("Please pass in either base_url or environment to construct the client") diff --git a/src/codecombat/core/__init__.py b/src/codecombat/core/__init__.py index b213a1a..2414955 100644 --- a/src/codecombat/core/__init__.py +++ b/src/codecombat/core/__init__.py @@ -1,8 +1,17 @@ # This file was auto-generated by Fern from our API Definition. from .api_error import ApiError +from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper from .datetime_utils import serialize_datetime from .jsonable_encoder import jsonable_encoder -from .remove_none_from_headers import remove_none_from_headers +from .remove_none_from_dict import remove_none_from_dict -__all__ = ["ApiError", "jsonable_encoder", "remove_none_from_headers", "serialize_datetime"] +__all__ = [ + "ApiError", + "AsyncClientWrapper", + "BaseClientWrapper", + "SyncClientWrapper", + "jsonable_encoder", + "remove_none_from_dict", + "serialize_datetime", +] diff --git a/src/codecombat/core/api_error.py b/src/codecombat/core/api_error.py index 43f4b45..2e9fc54 100644 --- a/src/codecombat/core/api_error.py +++ b/src/codecombat/core/api_error.py @@ -10,3 +10,6 @@ class ApiError(Exception): def __init__(self, *, status_code: typing.Optional[int] = None, body: typing.Any = None): self.status_code = status_code self.body = body + + def __str__(self) -> str: + return f"status_code: {self.status_code}, body: {self.body}" diff --git a/src/codecombat/core/client_wrapper.py b/src/codecombat/core/client_wrapper.py new file mode 100644 index 0000000..c1f18c0 --- /dev/null +++ b/src/codecombat/core/client_wrapper.py @@ -0,0 +1,68 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + + +class BaseClientWrapper: + def __init__( + self, + *, + username: typing.Union[str, typing.Callable[[], str]], + password: typing.Union[str, typing.Callable[[], str]], + base_url: str + ): + self._username = username + self._password = password + self._base_url = base_url + + def get_headers(self) -> typing.Dict[str, str]: + headers: typing.Dict[str, str] = { + "X-Fern-Language": "Python", + "X-Fern-SDK-Name": "codecombat", + "X-Fern-SDK-Version": "0.1.6", + } + headers["Authorization"] = httpx.BasicAuth(self._get_username(), self._get_password())._auth_header + return headers + + def _get_username(self) -> str: + if isinstance(self._username, str): + return self._username + else: + return self._username() + + def _get_password(self) -> str: + if isinstance(self._password, str): + return self._password + else: + return self._password() + + def get_base_url(self) -> str: + return self._base_url + + +class SyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + username: typing.Union[str, typing.Callable[[], str]], + password: typing.Union[str, typing.Callable[[], str]], + base_url: str, + httpx_client: httpx.Client + ): + super().__init__(username=username, password=password, base_url=base_url) + self.httpx_client = httpx_client + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + username: typing.Union[str, typing.Callable[[], str]], + password: typing.Union[str, typing.Callable[[], str]], + base_url: str, + httpx_client: httpx.AsyncClient + ): + super().__init__(username=username, password=password, base_url=base_url) + self.httpx_client = httpx_client diff --git a/src/codecombat/core/jsonable_encoder.py b/src/codecombat/core/jsonable_encoder.py index ed5a43e..5c3cfac 100644 --- a/src/codecombat/core/jsonable_encoder.py +++ b/src/codecombat/core/jsonable_encoder.py @@ -9,6 +9,7 @@ """ import dataclasses +import datetime as dt from collections import defaultdict from enum import Enum from pathlib import PurePath @@ -18,6 +19,8 @@ from pydantic import BaseModel from pydantic.json import ENCODERS_BY_TYPE +from .datetime_utils import serialize_datetime + SetIntStr = Set[Union[int, str]] DictIntStrAny = Dict[Union[int, str], Any] @@ -60,6 +63,10 @@ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any] return str(obj) if isinstance(obj, (str, int, float, type(None))): return obj + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) if isinstance(obj, dict): encoded_dict = {} allowed_keys = set(obj.keys()) diff --git a/src/codecombat/core/remove_none_from_dict.py b/src/codecombat/core/remove_none_from_dict.py new file mode 100644 index 0000000..2da30f7 --- /dev/null +++ b/src/codecombat/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +def remove_none_from_dict(original: Dict[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/src/codecombat/core/remove_none_from_headers.py b/src/codecombat/core/remove_none_from_headers.py deleted file mode 100644 index 21a44ca..0000000 --- a/src/codecombat/core/remove_none_from_headers.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from typing import Dict, Optional - - -def remove_none_from_headers(headers: Dict[str, Optional[str]]) -> Dict[str, str]: - new_headers: Dict[str, str] = {} - for header_key, header_value in headers.items(): - if header_value is not None: - new_headers[header_key] = header_value - return new_headers diff --git a/src/codecombat/environment.py b/src/codecombat/environment.py index 5a85059..f462853 100644 --- a/src/codecombat/environment.py +++ b/src/codecombat/environment.py @@ -4,4 +4,4 @@ class CodeCombatEnvironment(enum.Enum): - PRODUCTION = "https://codecombat.com/api" + DEFAULT = "https://codecombat.com/api" diff --git a/src/codecombat/resources/__init__.py b/src/codecombat/resources/__init__.py index b45844b..5b07bab 100644 --- a/src/codecombat/resources/__init__.py +++ b/src/codecombat/resources/__init__.py @@ -1,51 +1,5 @@ # This file was auto-generated by Fern from our API Definition. -from . import auth, clans, classrooms, commons, stats, users -from .clans import ClanResponse -from .classrooms import AceConfig, Level, LevelSessionResponse, MemberStat, PlayStats, State -from .commons import ( - AuthIdentity, - ClassroomResponse, - ClassroomResponseWithCode, - Course, - DatetimeString, - License, - ObjectIdString, - RoleString, - Subscription, - UserResponse, - UserStats, -) -from .stats import LicenseStatsResponse, PlaytimeStatsResponse -from .users import HeroConfig, UserRole +from . import auth, clans, classrooms, stats, users -__all__ = [ - "AceConfig", - "AuthIdentity", - "ClanResponse", - "ClassroomResponse", - "ClassroomResponseWithCode", - "Course", - "DatetimeString", - "HeroConfig", - "Level", - "LevelSessionResponse", - "License", - "LicenseStatsResponse", - "MemberStat", - "ObjectIdString", - "PlayStats", - "PlaytimeStatsResponse", - "RoleString", - "State", - "Subscription", - "UserResponse", - "UserRole", - "UserStats", - "auth", - "clans", - "classrooms", - "commons", - "stats", - "users", -] +__all__ = ["auth", "clans", "classrooms", "stats", "users"] diff --git a/src/codecombat/resources/auth/client.py b/src/codecombat/resources/auth/client.py index b7d6de6..751d488 100644 --- a/src/codecombat/resources/auth/client.py +++ b/src/codecombat/resources/auth/client.py @@ -4,21 +4,16 @@ import urllib.parse from json.decoder import JSONDecodeError -import httpx - from ...core.api_error import ApiError -from ...environment import CodeCombatEnvironment +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.remove_none_from_dict import remove_none_from_dict class AuthClient: - def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str - ): - self._environment = environment - self._username = username - self._password = password - - def get( + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def login_oauth( self, *, provider: str, @@ -27,17 +22,34 @@ def get( redirect: typing.Optional[str] = None, error_redirect: typing.Optional[str] = None, ) -> None: - _response = httpx.request( + """ + Logs a user in. In this example, we call your lookup URL (let's say, `https://oauth.provider/user?t=<%= accessToken %>`) with the access token (`1234`). The lookup URL returns `{ id: 'abcd' }` in this case. We will match this `id` with the OAuthIdentity stored in the user information in our db. If everything checks out, the user is logged in and redirected to the home page. + + Parameters: + - provider: str. Your OAuth Provider ID + + - access_token: typing.Optional[str]. Will be passed through your lookup URL to get the user ID. Required if no `code`. + + - code: typing.Optional[str]. Will be passed to the OAuth token endpoint to get a token. Required if no `accessToken`. + + - redirect: typing.Optional[str]. Override where the user will navigate to after successfully logging in. + + - error_redirect: typing.Optional[str]. If an error happens, redirects the user to this url, with at least query parameters `code`, `errorName` and `message`. + """ + _response = self._client_wrapper.httpx_client.request( "GET", - urllib.parse.urljoin(f"{self._environment.value}/", "auth/login-o-auth"), - params={ - "provider": provider, - "accessToken": access_token, - "code": code, - "redirect": redirect, - "errorRedirect": error_redirect, - }, - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "auth/login-o-auth"), + params=remove_none_from_dict( + { + "provider": provider, + "accessToken": access_token, + "code": code, + "redirect": redirect, + "errorRedirect": error_redirect, + } + ), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return @@ -49,14 +61,10 @@ def get( class AsyncAuthClient: - def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str - ): - self._environment = environment - self._username = username - self._password = password - - async def get( + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def login_oauth( self, *, provider: str, @@ -65,19 +73,35 @@ async def get( redirect: typing.Optional[str] = None, error_redirect: typing.Optional[str] = None, ) -> None: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "GET", - urllib.parse.urljoin(f"{self._environment.value}/", "auth/login-o-auth"), - params={ + """ + Logs a user in. In this example, we call your lookup URL (let's say, `https://oauth.provider/user?t=<%= accessToken %>`) with the access token (`1234`). The lookup URL returns `{ id: 'abcd' }` in this case. We will match this `id` with the OAuthIdentity stored in the user information in our db. If everything checks out, the user is logged in and redirected to the home page. + + Parameters: + - provider: str. Your OAuth Provider ID + + - access_token: typing.Optional[str]. Will be passed through your lookup URL to get the user ID. Required if no `code`. + + - code: typing.Optional[str]. Will be passed to the OAuth token endpoint to get a token. Required if no `accessToken`. + + - redirect: typing.Optional[str]. Override where the user will navigate to after successfully logging in. + + - error_redirect: typing.Optional[str]. If an error happens, redirects the user to this url, with at least query parameters `code`, `errorName` and `message`. + """ + _response = await self._client_wrapper.httpx_client.request( + "GET", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "auth/login-o-auth"), + params=remove_none_from_dict( + { "provider": provider, "accessToken": access_token, "code": code, "redirect": redirect, "errorRedirect": error_redirect, - }, - auth=(self._username, self._password), - ) + } + ), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return try: diff --git a/src/codecombat/resources/clans/__init__.py b/src/codecombat/resources/clans/__init__.py index 2acd14b..f3ea265 100644 --- a/src/codecombat/resources/clans/__init__.py +++ b/src/codecombat/resources/clans/__init__.py @@ -1,5 +1,2 @@ # This file was auto-generated by Fern from our API Definition. -from .types import ClanResponse - -__all__ = ["ClanResponse"] diff --git a/src/codecombat/resources/clans/client.py b/src/codecombat/resources/clans/client.py index 1db8e8d..226c823 100644 --- a/src/codecombat/resources/clans/client.py +++ b/src/codecombat/resources/clans/client.py @@ -1,31 +1,39 @@ # This file was auto-generated by Fern from our API Definition. +import typing import urllib.parse from json.decoder import JSONDecodeError -import httpx import pydantic from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ...core.jsonable_encoder import jsonable_encoder -from ...environment import CodeCombatEnvironment -from .types.clan_response import ClanResponse +from ...types.clan_response import ClanResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) class ClansClient: - def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str - ): - self._environment = environment - self._username = username - self._password = password - - def upsert_clan(self, handle: str, *, user_id: str) -> ClanResponse: - _response = httpx.request( + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def upsert_member(self, handle: str, *, user_id: str) -> ClanResponse: + """ + Upserts a user into the clan. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - user_id: str. The `_id` or `slug` of the user to add to the clan. + """ + _response = self._client_wrapper.httpx_client.request( "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"clan/{handle}/members"), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"clan/{handle}/members"), json=jsonable_encoder({"userId": user_id}), - auth=(self._username, self._password), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClanResponse, _response.json()) # type: ignore @@ -37,21 +45,25 @@ def upsert_clan(self, handle: str, *, user_id: str) -> ClanResponse: class AsyncClansClient: - def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str - ): - self._environment = environment - self._username = username - self._password = password - - async def upsert_clan(self, handle: str, *, user_id: str) -> ClanResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"clan/{handle}/members"), - json=jsonable_encoder({"userId": user_id}), - auth=(self._username, self._password), - ) + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def upsert_member(self, handle: str, *, user_id: str) -> ClanResponse: + """ + Upserts a user into the clan. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - user_id: str. The `_id` or `slug` of the user to add to the clan. + """ + _response = await self._client_wrapper.httpx_client.request( + "PUT", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"clan/{handle}/members"), + json=jsonable_encoder({"userId": user_id}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClanResponse, _response.json()) # type: ignore try: diff --git a/src/codecombat/resources/clans/types/__init__.py b/src/codecombat/resources/clans/types/__init__.py deleted file mode 100644 index 714fee3..0000000 --- a/src/codecombat/resources/clans/types/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .clan_response import ClanResponse - -__all__ = ["ClanResponse"] diff --git a/src/codecombat/resources/classrooms/__init__.py b/src/codecombat/resources/classrooms/__init__.py index 6f62b92..f3ea265 100644 --- a/src/codecombat/resources/classrooms/__init__.py +++ b/src/codecombat/resources/classrooms/__init__.py @@ -1,5 +1,2 @@ # This file was auto-generated by Fern from our API Definition. -from .types import AceConfig, Level, LevelSessionResponse, MemberStat, PlayStats, State - -__all__ = ["AceConfig", "Level", "LevelSessionResponse", "MemberStat", "PlayStats", "State"] diff --git a/src/codecombat/resources/classrooms/client.py b/src/codecombat/resources/classrooms/client.py index 9860f36..ada9b5a 100644 --- a/src/codecombat/resources/classrooms/client.py +++ b/src/codecombat/resources/classrooms/client.py @@ -4,34 +4,42 @@ import urllib.parse from json.decoder import JSONDecodeError -import httpx import pydantic from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ...core.jsonable_encoder import jsonable_encoder -from ...environment import CodeCombatEnvironment -from ..commons.types.classroom_response import ClassroomResponse -from ..commons.types.classroom_response_with_code import ClassroomResponseWithCode -from ..commons.types.object_id_string import ObjectIdString -from .types.ace_config import AceConfig -from .types.level_session_response import LevelSessionResponse -from .types.member_stat import MemberStat +from ...core.remove_none_from_dict import remove_none_from_dict +from ...types.classroom_response import ClassroomResponse +from ...types.classroom_response_with_code import ClassroomResponseWithCode +from ...types.classrooms_create_request_ace_config import ClassroomsCreateRequestAceConfig +from ...types.classrooms_get_members_stats_response_item import ClassroomsGetMembersStatsResponseItem +from ...types.level_session_response import LevelSessionResponse +from ...types.object_id_string import ObjectIdString + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) class ClassroomsClient: - def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str - ): - self._environment = environment - self._username = username - self._password = password + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper def get(self, *, code: str, ret_member_limit: typing.Optional[float] = None) -> ClassroomResponseWithCode: - _response = httpx.request( + """ + Returns the classroom details for a class code. + + Parameters: + - code: str. The classroom's `code`. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for the classroom + """ + _response = self._client_wrapper.httpx_client.request( "GET", - urllib.parse.urljoin(f"{self._environment.value}/", "classrooms"), - params={"code": code, "retMemberLimit": ret_member_limit}, - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "classrooms"), + params=remove_none_from_dict({"code": code, "retMemberLimit": ret_member_limit}), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClassroomResponseWithCode, _response.json()) # type: ignore @@ -41,29 +49,58 @@ def get(self, *, code: str, ret_member_limit: typing.Optional[float] = None) -> raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def create(self, *, name: str, owner_id: ObjectIdString, ace_config: AceConfig) -> None: - _response = httpx.request( + def create( + self, *, name: str, owner_id: ObjectIdString, ace_config: ClassroomsCreateRequestAceConfig + ) -> ClassroomResponseWithCode: + """ + Creates a new empty `Classroom`. + + Parameters: + - name: str. Name of the classroom + + - owner_id: ObjectIdString. + + - ace_config: ClassroomsCreateRequestAceConfig. + """ + _response = self._client_wrapper.httpx_client.request( "POST", - urllib.parse.urljoin(f"{self._environment.value}/", "classrooms"), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "classrooms"), json=jsonable_encoder({"name": name, "ownerID": owner_id, "aceConfig": ace_config}), - auth=(self._username, self._password), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: - return + return pydantic.parse_obj_as(ClassroomResponseWithCode, _response.json()) # type: ignore try: _response_json = _response.json() except JSONDecodeError: raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def upsert_from_classroom( - self, handle: str, *, code: str, user_id: str, ret_member_limit: typing.Optional[float] = None + def upsert_member( + self, handle: str, *, code: str, user_id: str, ret_member_limit: typing.Optional[float] = OMIT ) -> ClassroomResponse: - _response = httpx.request( + """ + Upserts a user into the classroom. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - code: str. The code for joining this classroom + + - user_id: str. The `_id` or `slug` of the user to add to the class. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for the classroom, the default value is 1000 + """ + _request: typing.Dict[str, typing.Any] = {"code": code, "userId": user_id} + if ret_member_limit is not OMIT: + _request["retMemberLimit"] = ret_member_limit + _response = self._client_wrapper.httpx_client.request( "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"classrooms/{handle}/members"), - json=jsonable_encoder({"code": code, "userId": user_id, "retMemberLimit": ret_member_limit}), - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"classrooms/{handle}/members"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClassroomResponse, _response.json()) # type: ignore @@ -73,14 +110,28 @@ def upsert_from_classroom( raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def delete_user_from_classroom( - self, handle: str, *, user_id: str, ret_member_limit: typing.Optional[float] = None + def remove_member( + self, handle: str, *, user_id: str, ret_member_limit: typing.Optional[float] = OMIT ) -> ClassroomResponse: - _response = httpx.request( + """ + Remove a user from the classroom. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - user_id: str. The `_id` or `slug` of the user to remove from the class. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for the classroom, the default value is 1000 + """ + _request: typing.Dict[str, typing.Any] = {"userId": user_id} + if ret_member_limit is not OMIT: + _request["retMemberLimit"] = ret_member_limit + _response = self._client_wrapper.httpx_client.request( "DELETE", - urllib.parse.urljoin(f"{self._environment.value}/", f"classrooms/{handle}/members"), - json=jsonable_encoder({"userId": user_id, "retMemberLimit": ret_member_limit}), - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"classrooms/{handle}/members"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClassroomResponse, _response.json()) # type: ignore @@ -98,14 +149,30 @@ def enroll_user_in_course( ret_member_limit: typing.Optional[float] = None, user_id: ObjectIdString, ) -> ClassroomResponse: - _response = httpx.request( + """ + Enrolls a user in a course in a classroom. + If the course is paid, user must have an active license. + User must be a member of the classroom. + + Parameters: + - classroom_handle: str. The classroom's `_id`. + + - course_handle: str. The course's `_id`. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for the classroom, the default value is 1000 + + - user_id: ObjectIdString. + """ + _response = self._client_wrapper.httpx_client.request( "PUT", urllib.parse.urljoin( - f"{self._environment.value}/", f"classrooms/{classroom_handle}/courses/{course_handle}/enrolled" + f"{self._client_wrapper.get_base_url()}/", + f"classrooms/{classroom_handle}/courses/{course_handle}/enrolled", ), - params={"retMemberLimit": ret_member_limit}, + params=remove_none_from_dict({"retMemberLimit": ret_member_limit}), json=jsonable_encoder({"userId": user_id}), - auth=(self._username, self._password), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClassroomResponse, _response.json()) # type: ignore @@ -115,7 +182,7 @@ def enroll_user_in_course( raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def remove_user_from_classroom( + def remove_enrolled_user( self, classroom_handle: str, course_handle: str, @@ -123,14 +190,28 @@ def remove_user_from_classroom( ret_member_limit: typing.Optional[float] = None, user_id: ObjectIdString, ) -> ClassroomResponse: - _response = httpx.request( + """ + Removes an enrolled user from a course in a classroom. + + Parameters: + - classroom_handle: str. The classroom's `_id`. + + - course_handle: str. The course's `_id`. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for the classroom, the default value is 1000 + + - user_id: ObjectIdString. + """ + _response = self._client_wrapper.httpx_client.request( "PUT", urllib.parse.urljoin( - f"{self._environment.value}/", f"classrooms/{classroom_handle}/courses/{course_handle}/remove-enrolled" + f"{self._client_wrapper.get_base_url()}/", + f"classrooms/{classroom_handle}/courses/{course_handle}/remove-enrolled", ), - params={"retMemberLimit": ret_member_limit}, + params=remove_none_from_dict({"retMemberLimit": ret_member_limit}), json=jsonable_encoder({"userId": user_id}), - auth=(self._username, self._password), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClassroomResponse, _response.json()) # type: ignore @@ -147,28 +228,52 @@ def get_members_stats( project: typing.Optional[str] = None, member_limit: typing.Optional[float] = None, member_skip: typing.Optional[float] = None, - ) -> typing.List[MemberStat]: - _response = httpx.request( + ) -> typing.List[ClassroomsGetMembersStatsResponseItem]: + """ + Returns a list of all members stats for the classroom. + + Parameters: + - classroom_handle: str. The classroom's `_id`. + + - project: typing.Optional[str]. If specified, include only the specified projection of returned stats; else, return all stats. Format as a comma-separated list, like `creator,playtime,state.complete`. + + - member_limit: typing.Optional[float]. Limit the return member number. the default value is 10, and the max value is 100 + + - member_skip: typing.Optional[float]. Skip the members that doesn't need to return, for pagination + + """ + _response = self._client_wrapper.httpx_client.request( "GET", - urllib.parse.urljoin(f"{self._environment.value}/", f"classrooms/{classroom_handle}/stats"), - params={"project": project, "memberLimit": member_limit, "memberSkip": member_skip}, - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"classrooms/{classroom_handle}/stats"), + params=remove_none_from_dict({"project": project, "memberLimit": member_limit, "memberSkip": member_skip}), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: - return pydantic.parse_obj_as(typing.List[MemberStat], _response.json()) # type: ignore + return pydantic.parse_obj_as(typing.List[ClassroomsGetMembersStatsResponseItem], _response.json()) # type: ignore try: _response_json = _response.json() except JSONDecodeError: raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def get_level_session(self, classroom_handle: str, member_handle: str) -> typing.List[LevelSessionResponse]: - _response = httpx.request( + def get_levels_played(self, classroom_handle: str, member_handle: str) -> typing.List[LevelSessionResponse]: + """ + Returns a list of all levels played by the user for the classroom. + + Parameters: + - classroom_handle: str. The classroom's `_id`. + + - member_handle: str. The classroom member's `_id`. + """ + _response = self._client_wrapper.httpx_client.request( "GET", urllib.parse.urljoin( - f"{self._environment.value}/", f"classrooms/{classroom_handle}/members/{member_handle}/sessions" + f"{self._client_wrapper.get_base_url()}/", + f"classrooms/{classroom_handle}/members/{member_handle}/sessions", ), - auth=(self._username, self._password), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[LevelSessionResponse], _response.json()) # type: ignore @@ -180,21 +285,25 @@ def get_level_session(self, classroom_handle: str, member_handle: str) -> typing class AsyncClassroomsClient: - def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str - ): - self._environment = environment - self._username = username - self._password = password + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper async def get(self, *, code: str, ret_member_limit: typing.Optional[float] = None) -> ClassroomResponseWithCode: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "GET", - urllib.parse.urljoin(f"{self._environment.value}/", "classrooms"), - params={"code": code, "retMemberLimit": ret_member_limit}, - auth=(self._username, self._password), - ) + """ + Returns the classroom details for a class code. + + Parameters: + - code: str. The classroom's `code`. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for the classroom + """ + _response = await self._client_wrapper.httpx_client.request( + "GET", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "classrooms"), + params=remove_none_from_dict({"code": code, "retMemberLimit": ret_member_limit}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClassroomResponseWithCode, _response.json()) # type: ignore try: @@ -203,32 +312,59 @@ async def get(self, *, code: str, ret_member_limit: typing.Optional[float] = Non raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def create(self, *, name: str, owner_id: ObjectIdString, ace_config: AceConfig) -> None: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "POST", - urllib.parse.urljoin(f"{self._environment.value}/", "classrooms"), - json=jsonable_encoder({"name": name, "ownerID": owner_id, "aceConfig": ace_config}), - auth=(self._username, self._password), - ) + async def create( + self, *, name: str, owner_id: ObjectIdString, ace_config: ClassroomsCreateRequestAceConfig + ) -> ClassroomResponseWithCode: + """ + Creates a new empty `Classroom`. + + Parameters: + - name: str. Name of the classroom + + - owner_id: ObjectIdString. + + - ace_config: ClassroomsCreateRequestAceConfig. + """ + _response = await self._client_wrapper.httpx_client.request( + "POST", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "classrooms"), + json=jsonable_encoder({"name": name, "ownerID": owner_id, "aceConfig": ace_config}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: - return + return pydantic.parse_obj_as(ClassroomResponseWithCode, _response.json()) # type: ignore try: _response_json = _response.json() except JSONDecodeError: raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def upsert_from_classroom( - self, handle: str, *, code: str, user_id: str, ret_member_limit: typing.Optional[float] = None + async def upsert_member( + self, handle: str, *, code: str, user_id: str, ret_member_limit: typing.Optional[float] = OMIT ) -> ClassroomResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"classrooms/{handle}/members"), - json=jsonable_encoder({"code": code, "userId": user_id, "retMemberLimit": ret_member_limit}), - auth=(self._username, self._password), - ) + """ + Upserts a user into the classroom. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - code: str. The code for joining this classroom + + - user_id: str. The `_id` or `slug` of the user to add to the class. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for the classroom, the default value is 1000 + """ + _request: typing.Dict[str, typing.Any] = {"code": code, "userId": user_id} + if ret_member_limit is not OMIT: + _request["retMemberLimit"] = ret_member_limit + _response = await self._client_wrapper.httpx_client.request( + "PUT", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"classrooms/{handle}/members"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClassroomResponse, _response.json()) # type: ignore try: @@ -237,16 +373,29 @@ async def upsert_from_classroom( raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def delete_user_from_classroom( - self, handle: str, *, user_id: str, ret_member_limit: typing.Optional[float] = None + async def remove_member( + self, handle: str, *, user_id: str, ret_member_limit: typing.Optional[float] = OMIT ) -> ClassroomResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "DELETE", - urllib.parse.urljoin(f"{self._environment.value}/", f"classrooms/{handle}/members"), - json=jsonable_encoder({"userId": user_id, "retMemberLimit": ret_member_limit}), - auth=(self._username, self._password), - ) + """ + Remove a user from the classroom. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - user_id: str. The `_id` or `slug` of the user to remove from the class. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for the classroom, the default value is 1000 + """ + _request: typing.Dict[str, typing.Any] = {"userId": user_id} + if ret_member_limit is not OMIT: + _request["retMemberLimit"] = ret_member_limit + _response = await self._client_wrapper.httpx_client.request( + "DELETE", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"classrooms/{handle}/members"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClassroomResponse, _response.json()) # type: ignore try: @@ -263,16 +412,31 @@ async def enroll_user_in_course( ret_member_limit: typing.Optional[float] = None, user_id: ObjectIdString, ) -> ClassroomResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "PUT", - urllib.parse.urljoin( - f"{self._environment.value}/", f"classrooms/{classroom_handle}/courses/{course_handle}/enrolled" - ), - params={"retMemberLimit": ret_member_limit}, - json=jsonable_encoder({"userId": user_id}), - auth=(self._username, self._password), - ) + """ + Enrolls a user in a course in a classroom. + If the course is paid, user must have an active license. + User must be a member of the classroom. + + Parameters: + - classroom_handle: str. The classroom's `_id`. + + - course_handle: str. The course's `_id`. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for the classroom, the default value is 1000 + + - user_id: ObjectIdString. + """ + _response = await self._client_wrapper.httpx_client.request( + "PUT", + urllib.parse.urljoin( + f"{self._client_wrapper.get_base_url()}/", + f"classrooms/{classroom_handle}/courses/{course_handle}/enrolled", + ), + params=remove_none_from_dict({"retMemberLimit": ret_member_limit}), + json=jsonable_encoder({"userId": user_id}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClassroomResponse, _response.json()) # type: ignore try: @@ -281,7 +445,7 @@ async def enroll_user_in_course( raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def remove_user_from_classroom( + async def remove_enrolled_user( self, classroom_handle: str, course_handle: str, @@ -289,17 +453,29 @@ async def remove_user_from_classroom( ret_member_limit: typing.Optional[float] = None, user_id: ObjectIdString, ) -> ClassroomResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "PUT", - urllib.parse.urljoin( - f"{self._environment.value}/", - f"classrooms/{classroom_handle}/courses/{course_handle}/remove-enrolled", - ), - params={"retMemberLimit": ret_member_limit}, - json=jsonable_encoder({"userId": user_id}), - auth=(self._username, self._password), - ) + """ + Removes an enrolled user from a course in a classroom. + + Parameters: + - classroom_handle: str. The classroom's `_id`. + + - course_handle: str. The course's `_id`. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for the classroom, the default value is 1000 + + - user_id: ObjectIdString. + """ + _response = await self._client_wrapper.httpx_client.request( + "PUT", + urllib.parse.urljoin( + f"{self._client_wrapper.get_base_url()}/", + f"classrooms/{classroom_handle}/courses/{course_handle}/remove-enrolled", + ), + params=remove_none_from_dict({"retMemberLimit": ret_member_limit}), + json=jsonable_encoder({"userId": user_id}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(ClassroomResponse, _response.json()) # type: ignore try: @@ -315,31 +491,53 @@ async def get_members_stats( project: typing.Optional[str] = None, member_limit: typing.Optional[float] = None, member_skip: typing.Optional[float] = None, - ) -> typing.List[MemberStat]: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "GET", - urllib.parse.urljoin(f"{self._environment.value}/", f"classrooms/{classroom_handle}/stats"), - params={"project": project, "memberLimit": member_limit, "memberSkip": member_skip}, - auth=(self._username, self._password), - ) + ) -> typing.List[ClassroomsGetMembersStatsResponseItem]: + """ + Returns a list of all members stats for the classroom. + + Parameters: + - classroom_handle: str. The classroom's `_id`. + + - project: typing.Optional[str]. If specified, include only the specified projection of returned stats; else, return all stats. Format as a comma-separated list, like `creator,playtime,state.complete`. + + - member_limit: typing.Optional[float]. Limit the return member number. the default value is 10, and the max value is 100 + + - member_skip: typing.Optional[float]. Skip the members that doesn't need to return, for pagination + + """ + _response = await self._client_wrapper.httpx_client.request( + "GET", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"classrooms/{classroom_handle}/stats"), + params=remove_none_from_dict({"project": project, "memberLimit": member_limit, "memberSkip": member_skip}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: - return pydantic.parse_obj_as(typing.List[MemberStat], _response.json()) # type: ignore + return pydantic.parse_obj_as(typing.List[ClassroomsGetMembersStatsResponseItem], _response.json()) # type: ignore try: _response_json = _response.json() except JSONDecodeError: raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def get_level_session(self, classroom_handle: str, member_handle: str) -> typing.List[LevelSessionResponse]: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "GET", - urllib.parse.urljoin( - f"{self._environment.value}/", f"classrooms/{classroom_handle}/members/{member_handle}/sessions" - ), - auth=(self._username, self._password), - ) + async def get_levels_played(self, classroom_handle: str, member_handle: str) -> typing.List[LevelSessionResponse]: + """ + Returns a list of all levels played by the user for the classroom. + + Parameters: + - classroom_handle: str. The classroom's `_id`. + + - member_handle: str. The classroom member's `_id`. + """ + _response = await self._client_wrapper.httpx_client.request( + "GET", + urllib.parse.urljoin( + f"{self._client_wrapper.get_base_url()}/", + f"classrooms/{classroom_handle}/members/{member_handle}/sessions", + ), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[LevelSessionResponse], _response.json()) # type: ignore try: diff --git a/src/codecombat/resources/classrooms/types/__init__.py b/src/codecombat/resources/classrooms/types/__init__.py deleted file mode 100644 index 1827bea..0000000 --- a/src/codecombat/resources/classrooms/types/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .ace_config import AceConfig -from .level import Level -from .level_session_response import LevelSessionResponse -from .member_stat import MemberStat -from .play_stats import PlayStats -from .state import State - -__all__ = ["AceConfig", "Level", "LevelSessionResponse", "MemberStat", "PlayStats", "State"] diff --git a/src/codecombat/resources/commons/__init__.py b/src/codecombat/resources/commons/__init__.py deleted file mode 100644 index a1de063..0000000 --- a/src/codecombat/resources/commons/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .types import ( - AuthIdentity, - ClassroomResponse, - ClassroomResponseWithCode, - Course, - DatetimeString, - License, - ObjectIdString, - RoleString, - Subscription, - UserResponse, - UserStats, -) - -__all__ = [ - "AuthIdentity", - "ClassroomResponse", - "ClassroomResponseWithCode", - "Course", - "DatetimeString", - "License", - "ObjectIdString", - "RoleString", - "Subscription", - "UserResponse", - "UserStats", -] diff --git a/src/codecombat/resources/commons/types/__init__.py b/src/codecombat/resources/commons/types/__init__.py deleted file mode 100644 index 60b0ece..0000000 --- a/src/codecombat/resources/commons/types/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .auth_identity import AuthIdentity -from .classroom_response import ClassroomResponse -from .classroom_response_with_code import ClassroomResponseWithCode -from .course import Course -from .datetime_string import DatetimeString -from .license import License -from .object_id_string import ObjectIdString -from .role_string import RoleString -from .subscription import Subscription -from .user_response import UserResponse -from .user_stats import UserStats - -__all__ = [ - "AuthIdentity", - "ClassroomResponse", - "ClassroomResponseWithCode", - "Course", - "DatetimeString", - "License", - "ObjectIdString", - "RoleString", - "Subscription", - "UserResponse", - "UserStats", -] diff --git a/src/codecombat/resources/stats/__init__.py b/src/codecombat/resources/stats/__init__.py index d7101da..f3ea265 100644 --- a/src/codecombat/resources/stats/__init__.py +++ b/src/codecombat/resources/stats/__init__.py @@ -1,5 +1,2 @@ # This file was auto-generated by Fern from our API Definition. -from .types import LicenseStatsResponse, PlaytimeStatsResponse - -__all__ = ["LicenseStatsResponse", "PlaytimeStatsResponse"] diff --git a/src/codecombat/resources/stats/client.py b/src/codecombat/resources/stats/client.py index 80195ae..4c09ce5 100644 --- a/src/codecombat/resources/stats/client.py +++ b/src/codecombat/resources/stats/client.py @@ -4,22 +4,18 @@ import urllib.parse from json.decoder import JSONDecodeError -import httpx import pydantic from ...core.api_error import ApiError -from ...environment import CodeCombatEnvironment -from .types.license_stats_response import LicenseStatsResponse -from .types.playtime_stats_response import PlaytimeStatsResponse +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.remove_none_from_dict import remove_none_from_dict +from ...types.license_stats_response import LicenseStatsResponse +from ...types.playtime_stats_response import PlaytimeStatsResponse class StatsClient: - def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str - ): - self._environment = environment - self._username = username - self._password = password + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper def get_playtime_stats( self, @@ -28,11 +24,22 @@ def get_playtime_stats( end_date: typing.Optional[str] = None, country: typing.Optional[str] = None, ) -> PlaytimeStatsResponse: - _response = httpx.request( + """ + Returns the playtime stats + + Parameters: + - start_date: typing.Optional[str]. Earliest an included user was created + + - end_date: typing.Optional[str]. Latest an included user was created + + - country: typing.Optional[str]. Filter by country string + """ + _response = self._client_wrapper.httpx_client.request( "GET", - urllib.parse.urljoin(f"{self._environment.value}/", "playtime-stats"), - params={"startDate": start_date, "endDate": end_date, "country": country}, - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "playtime-stats"), + params=remove_none_from_dict({"startDate": start_date, "endDate": end_date, "country": country}), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(PlaytimeStatsResponse, _response.json()) # type: ignore @@ -43,10 +50,14 @@ def get_playtime_stats( raise ApiError(status_code=_response.status_code, body=_response_json) def get_license_stats(self) -> LicenseStatsResponse: - _response = httpx.request( + """ + Returns the license stats + """ + _response = self._client_wrapper.httpx_client.request( "GET", - urllib.parse.urljoin(f"{self._environment.value}/", "license-stats"), - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "license-stats"), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(LicenseStatsResponse, _response.json()) # type: ignore @@ -58,12 +69,8 @@ def get_license_stats(self) -> LicenseStatsResponse: class AsyncStatsClient: - def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str - ): - self._environment = environment - self._username = username - self._password = password + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper async def get_playtime_stats( self, @@ -72,13 +79,23 @@ async def get_playtime_stats( end_date: typing.Optional[str] = None, country: typing.Optional[str] = None, ) -> PlaytimeStatsResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "GET", - urllib.parse.urljoin(f"{self._environment.value}/", "playtime-stats"), - params={"startDate": start_date, "endDate": end_date, "country": country}, - auth=(self._username, self._password), - ) + """ + Returns the playtime stats + + Parameters: + - start_date: typing.Optional[str]. Earliest an included user was created + + - end_date: typing.Optional[str]. Latest an included user was created + + - country: typing.Optional[str]. Filter by country string + """ + _response = await self._client_wrapper.httpx_client.request( + "GET", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "playtime-stats"), + params=remove_none_from_dict({"startDate": start_date, "endDate": end_date, "country": country}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(PlaytimeStatsResponse, _response.json()) # type: ignore try: @@ -88,12 +105,15 @@ async def get_playtime_stats( raise ApiError(status_code=_response.status_code, body=_response_json) async def get_license_stats(self) -> LicenseStatsResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "GET", - urllib.parse.urljoin(f"{self._environment.value}/", "license-stats"), - auth=(self._username, self._password), - ) + """ + Returns the license stats + """ + _response = await self._client_wrapper.httpx_client.request( + "GET", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "license-stats"), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(LicenseStatsResponse, _response.json()) # type: ignore try: diff --git a/src/codecombat/resources/stats/types/__init__.py b/src/codecombat/resources/stats/types/__init__.py deleted file mode 100644 index c374ee9..0000000 --- a/src/codecombat/resources/stats/types/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .license_stats_response import LicenseStatsResponse -from .playtime_stats_response import PlaytimeStatsResponse - -__all__ = ["LicenseStatsResponse", "PlaytimeStatsResponse"] diff --git a/src/codecombat/resources/users/__init__.py b/src/codecombat/resources/users/__init__.py index d1232d6..f3ea265 100644 --- a/src/codecombat/resources/users/__init__.py +++ b/src/codecombat/resources/users/__init__.py @@ -1,5 +1,2 @@ # This file was auto-generated by Fern from our API Definition. -from .types import HeroConfig, UserRole - -__all__ = ["HeroConfig", "UserRole"] diff --git a/src/codecombat/resources/users/client.py b/src/codecombat/resources/users/client.py index be0db8b..6535915 100644 --- a/src/codecombat/resources/users/client.py +++ b/src/codecombat/resources/users/client.py @@ -1,58 +1,74 @@ # This file was auto-generated by Fern from our API Definition. -import datetime as dt import typing import urllib.parse from json.decoder import JSONDecodeError -import httpx import pydantic from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ...core.jsonable_encoder import jsonable_encoder -from ...environment import CodeCombatEnvironment -from ..commons.types.classroom_response_with_code import ClassroomResponseWithCode -from ..commons.types.object_id_string import ObjectIdString -from ..commons.types.user_response import UserResponse -from .types.hero_config import HeroConfig -from .types.user_role import UserRole +from ...core.remove_none_from_dict import remove_none_from_dict +from ...types.classroom_response_with_code import ClassroomResponseWithCode +from ...types.datetime_string import DatetimeString +from ...types.object_id_string import ObjectIdString +from ...types.user_response import UserResponse +from ...types.users_create_request_hero_config import UsersCreateRequestHeroConfig +from ...types.users_create_request_role import UsersCreateRequestRole + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) class UsersClient: - def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str - ): - self._environment = environment - self._username = username - self._password = password + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper def create( self, *, name: str, email: str, - role: typing.Optional[UserRole] = None, - preferred_language: typing.Optional[str] = None, - hero_config: typing.Optional[HeroConfig] = None, - birthday: typing.Optional[str] = None, - ) -> None: - _response = httpx.request( + role: typing.Optional[UsersCreateRequestRole] = OMIT, + preferred_language: typing.Optional[str] = OMIT, + hero_config: typing.Optional[UsersCreateRequestHeroConfig] = OMIT, + birthday: typing.Optional[str] = OMIT, + ) -> UserResponse: + """ + Creates a `User`. + + Parameters: + - name: str. + + - email: str. + + - role: typing.Optional[UsersCreateRequestRole]. `"student"` or `"teacher"`. If unset, a home user will be created, unable to join classrooms. + + - preferred_language: typing.Optional[str]. + + - hero_config: typing.Optional[UsersCreateRequestHeroConfig]. + + - birthday: typing.Optional[str]. + """ + _request: typing.Dict[str, typing.Any] = {"name": name, "email": email} + if role is not OMIT: + _request["role"] = role + if preferred_language is not OMIT: + _request["preferredLanguage"] = preferred_language + if hero_config is not OMIT: + _request["heroConfig"] = hero_config + if birthday is not OMIT: + _request["birthday"] = birthday + _response = self._client_wrapper.httpx_client.request( "POST", - urllib.parse.urljoin(f"{self._environment.value}/", "users"), - json=jsonable_encoder( - { - "name": name, - "email": email, - "role": role, - "preferredLanguage": preferred_language, - "heroConfig": hero_config, - "birthday": birthday, - } - ), - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "users"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: - return + return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore try: _response_json = _response.json() except JSONDecodeError: @@ -60,11 +76,20 @@ def create( raise ApiError(status_code=_response.status_code, body=_response_json) def get(self, handle: str, *, include_play_time: typing.Optional[str] = None) -> UserResponse: - _response = httpx.request( + """ + Returns a `User`. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - include_play_time: typing.Optional[str]. Set to non-empty string to include stats.playTime in response + """ + _response = self._client_wrapper.httpx_client.request( "GET", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}"), - params={"includePlayTime": include_play_time}, - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}"), + params=remove_none_from_dict({"includePlayTime": include_play_time}), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore @@ -74,12 +99,26 @@ def get(self, handle: str, *, include_play_time: typing.Optional[str] = None) -> raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def update(self, handle: str, *, name: str, birthday: typing.Optional[str] = None) -> UserResponse: - _response = httpx.request( + def update(self, handle: str, *, name: str, birthday: typing.Optional[str] = OMIT) -> UserResponse: + """ + Modify name of a `User` + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - name: str. Set to new name string + + - birthday: typing.Optional[str]. Set the birthday + """ + _request: typing.Dict[str, typing.Any] = {"name": name} + if birthday is not OMIT: + _request["birthday"] = birthday + _response = self._client_wrapper.httpx_client.request( "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}"), - json=jsonable_encoder({"name": name, "birthday": birthday}), - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore @@ -92,11 +131,20 @@ def update(self, handle: str, *, name: str, birthday: typing.Optional[str] = Non def get_classrooms( self, handle: str, *, ret_member_limit: typing.Optional[float] = None ) -> typing.List[ClassroomResponseWithCode]: - _response = httpx.request( + """ + Returns a list of `Classrooms` this user is in (if a student) or owns (if a teacher). + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for each classroom + """ + _response = self._client_wrapper.httpx_client.request( "GET", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/classrooms"), - params={"retMemberLimit": ret_member_limit}, - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/classrooms"), + params=remove_none_from_dict({"retMemberLimit": ret_member_limit}), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[ClassroomResponseWithCode], _response.json()) # type: ignore @@ -106,12 +154,24 @@ def get_classrooms( raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def get_hero(self, handle: str, *, thang_type: typing.Optional[ObjectIdString] = None) -> UserResponse: - _response = httpx.request( + def set_hero(self, handle: str, *, thang_type: typing.Optional[ObjectIdString] = OMIT) -> UserResponse: + """ + Set the user's hero. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - thang_type: typing.Optional[ObjectIdString]. + """ + _request: typing.Dict[str, typing.Any] = {} + if thang_type is not OMIT: + _request["thangType"] = thang_type + _response = self._client_wrapper.httpx_client.request( "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/hero-config"), - json=jsonable_encoder({"thangType": thang_type}), - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/hero-config"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore @@ -125,15 +185,35 @@ def set_ace_config( self, handle: str, *, - live_completion: typing.Optional[bool] = None, - behaviors: typing.Optional[bool] = None, - language: typing.Optional[str] = None, + live_completion: typing.Optional[bool] = OMIT, + behaviors: typing.Optional[bool] = OMIT, + language: typing.Optional[str] = OMIT, ) -> UserResponse: - _response = httpx.request( + """ + Set the user's aceConfig (the settings for the in-game Ace code editor), such as whether to enable autocomplete. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - live_completion: typing.Optional[bool]. controls whether autocompletion snippets show up, the default value is true + + - behaviors: typing.Optional[bool]. controls whether things like automatic parenthesis and quote completion happens, the default value is false + + - language: typing.Optional[str]. only for home users, should be one of ["python", "javascript", "cpp", "lua", "coffeescript"] right now + """ + _request: typing.Dict[str, typing.Any] = {} + if live_completion is not OMIT: + _request["liveCompletion"] = live_completion + if behaviors is not OMIT: + _request["behaviors"] = behaviors + if language is not OMIT: + _request["language"] = language + _response = self._client_wrapper.httpx_client.request( "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/ace-config"), - json=jsonable_encoder({"liveCompletion": live_completion, "behaviors": behaviors, "language": language}), - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/ace-config"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore @@ -143,34 +223,21 @@ def set_ace_config( raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def add_o_auth_identity( - self, - handle: str, - *, - provider: str, - access_token: typing.Optional[str] = None, - code: typing.Optional[str] = None, - ) -> UserResponse: - _response = httpx.request( - "POST", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/o-auth-identities"), - json=jsonable_encoder({"provider": provider, "accessToken": access_token, "code": code}), - auth=(self._username, self._password), - ) - if 200 <= _response.status_code < 300: - return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore - try: - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) + def grant_premium_subscription(self, handle: str, *, ends: DatetimeString) -> UserResponse: + """ + Grants a user premium access to the "Home" version up to a certain time. - def update_subscription(self, handle: str, *, ends: dt.datetime) -> UserResponse: - _response = httpx.request( + Parameters: + - handle: str. The document's `_id` or `slug`. + + - ends: DatetimeString. + """ + _response = self._client_wrapper.httpx_client.request( "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/subscription"), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/subscription"), json=jsonable_encoder({"ends": ends}), - auth=(self._username, self._password), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore @@ -180,12 +247,21 @@ def update_subscription(self, handle: str, *, ends: dt.datetime) -> UserResponse raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def shorten_subscription(self, handle: str, *, ends: dt.datetime) -> UserResponse: - _response = httpx.request( + def shorten_subscription(self, handle: str, *, ends: DatetimeString) -> UserResponse: + """ + If the user already has a premium access up to a certain time, this shortens/revokes his/her premium access. If the ends is less than or equal to the current time, it revokes the subscription and sets the end date to be the current time, else it just shortens the subscription. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - ends: DatetimeString. + """ + _response = self._client_wrapper.httpx_client.request( "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/shorten-subscription"), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/shorten-subscription"), json=jsonable_encoder({"ends": ends}), - auth=(self._username, self._password), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore @@ -195,12 +271,22 @@ def shorten_subscription(self, handle: str, *, ends: dt.datetime) -> UserRespons raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def grant_license(self, handle: str, *, ends: dt.datetime) -> UserResponse: - _response = httpx.request( + def grant_license(self, handle: str, *, ends: DatetimeString) -> UserResponse: + """ + Grants a user access to the "Classroom" version up to a certain time. + Sets their role to "student". + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - ends: DatetimeString. + """ + _response = self._client_wrapper.httpx_client.request( "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/license"), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/license"), json=jsonable_encoder({"ends": ends}), - auth=(self._username, self._password), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore @@ -210,12 +296,21 @@ def grant_license(self, handle: str, *, ends: dt.datetime) -> UserResponse: raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def shorten_license(self, handle: str, *, ends: dt.datetime) -> UserResponse: - _response = httpx.request( + def shorten_license(self, handle: str, *, ends: DatetimeString) -> UserResponse: + """ + If the user already has access to the "Classroom" version up to a certain time, this shortens/revokes his/her access. If the ends is less than or equal to the current time, it revokes the enrollment and sets the end date to be the current time, else it just shortens the enrollment. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - ends: DatetimeString. + """ + _response = self._client_wrapper.httpx_client.request( "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/shorten-license"), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/shorten-license"), json=jsonable_encoder({"ends": ends}), - auth=(self._username, self._password), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore @@ -225,11 +320,20 @@ def shorten_license(self, handle: str, *, ends: dt.datetime) -> UserResponse: raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - def find_user(self, property: str, value: str) -> None: - _response = httpx.request( + def lookup(self, property: str, value: str) -> None: + """ + Redirects to `/users/{handle}` given a unique, identifying property + + Parameters: + - property: str. The property to lookup by. May either be `"israel-id"` or `"name"`. + + - value: str. The value to be looked up. + """ + _response = self._client_wrapper.httpx_client.request( "GET", - urllib.parse.urljoin(f"{self._environment.value}/", f"user-lookup/{property}/{value}"), - auth=(self._username, self._password), + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"user-lookup/{property}/{value}"), + headers=self._client_wrapper.get_headers(), + timeout=60, ) if 200 <= _response.status_code < 300: return @@ -241,41 +345,53 @@ def find_user(self, property: str, value: str) -> None: class AsyncUsersClient: - def __init__( - self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str - ): - self._environment = environment - self._username = username - self._password = password + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper async def create( self, *, name: str, email: str, - role: typing.Optional[UserRole] = None, - preferred_language: typing.Optional[str] = None, - hero_config: typing.Optional[HeroConfig] = None, - birthday: typing.Optional[str] = None, - ) -> None: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "POST", - urllib.parse.urljoin(f"{self._environment.value}/", "users"), - json=jsonable_encoder( - { - "name": name, - "email": email, - "role": role, - "preferredLanguage": preferred_language, - "heroConfig": hero_config, - "birthday": birthday, - } - ), - auth=(self._username, self._password), - ) + role: typing.Optional[UsersCreateRequestRole] = OMIT, + preferred_language: typing.Optional[str] = OMIT, + hero_config: typing.Optional[UsersCreateRequestHeroConfig] = OMIT, + birthday: typing.Optional[str] = OMIT, + ) -> UserResponse: + """ + Creates a `User`. + + Parameters: + - name: str. + + - email: str. + + - role: typing.Optional[UsersCreateRequestRole]. `"student"` or `"teacher"`. If unset, a home user will be created, unable to join classrooms. + + - preferred_language: typing.Optional[str]. + + - hero_config: typing.Optional[UsersCreateRequestHeroConfig]. + + - birthday: typing.Optional[str]. + """ + _request: typing.Dict[str, typing.Any] = {"name": name, "email": email} + if role is not OMIT: + _request["role"] = role + if preferred_language is not OMIT: + _request["preferredLanguage"] = preferred_language + if hero_config is not OMIT: + _request["heroConfig"] = hero_config + if birthday is not OMIT: + _request["birthday"] = birthday + _response = await self._client_wrapper.httpx_client.request( + "POST", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "users"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: - return + return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore try: _response_json = _response.json() except JSONDecodeError: @@ -283,13 +399,21 @@ async def create( raise ApiError(status_code=_response.status_code, body=_response_json) async def get(self, handle: str, *, include_play_time: typing.Optional[str] = None) -> UserResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "GET", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}"), - params={"includePlayTime": include_play_time}, - auth=(self._username, self._password), - ) + """ + Returns a `User`. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - include_play_time: typing.Optional[str]. Set to non-empty string to include stats.playTime in response + """ + _response = await self._client_wrapper.httpx_client.request( + "GET", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}"), + params=remove_none_from_dict({"includePlayTime": include_play_time}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore try: @@ -298,14 +422,27 @@ async def get(self, handle: str, *, include_play_time: typing.Optional[str] = No raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def update(self, handle: str, *, name: str, birthday: typing.Optional[str] = None) -> UserResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}"), - json=jsonable_encoder({"name": name, "birthday": birthday}), - auth=(self._username, self._password), - ) + async def update(self, handle: str, *, name: str, birthday: typing.Optional[str] = OMIT) -> UserResponse: + """ + Modify name of a `User` + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - name: str. Set to new name string + + - birthday: typing.Optional[str]. Set the birthday + """ + _request: typing.Dict[str, typing.Any] = {"name": name} + if birthday is not OMIT: + _request["birthday"] = birthday + _response = await self._client_wrapper.httpx_client.request( + "PUT", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore try: @@ -317,13 +454,21 @@ async def update(self, handle: str, *, name: str, birthday: typing.Optional[str] async def get_classrooms( self, handle: str, *, ret_member_limit: typing.Optional[float] = None ) -> typing.List[ClassroomResponseWithCode]: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "GET", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/classrooms"), - params={"retMemberLimit": ret_member_limit}, - auth=(self._username, self._password), - ) + """ + Returns a list of `Classrooms` this user is in (if a student) or owns (if a teacher). + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - ret_member_limit: typing.Optional[float]. limit the return number of members for each classroom + """ + _response = await self._client_wrapper.httpx_client.request( + "GET", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/classrooms"), + params=remove_none_from_dict({"retMemberLimit": ret_member_limit}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(typing.List[ClassroomResponseWithCode], _response.json()) # type: ignore try: @@ -332,14 +477,25 @@ async def get_classrooms( raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def get_hero(self, handle: str, *, thang_type: typing.Optional[ObjectIdString] = None) -> UserResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/hero-config"), - json=jsonable_encoder({"thangType": thang_type}), - auth=(self._username, self._password), - ) + async def set_hero(self, handle: str, *, thang_type: typing.Optional[ObjectIdString] = OMIT) -> UserResponse: + """ + Set the user's hero. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - thang_type: typing.Optional[ObjectIdString]. + """ + _request: typing.Dict[str, typing.Any] = {} + if thang_type is not OMIT: + _request["thangType"] = thang_type + _response = await self._client_wrapper.httpx_client.request( + "PUT", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/hero-config"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore try: @@ -352,42 +508,36 @@ async def set_ace_config( self, handle: str, *, - live_completion: typing.Optional[bool] = None, - behaviors: typing.Optional[bool] = None, - language: typing.Optional[str] = None, + live_completion: typing.Optional[bool] = OMIT, + behaviors: typing.Optional[bool] = OMIT, + language: typing.Optional[str] = OMIT, ) -> UserResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/ace-config"), - json=jsonable_encoder( - {"liveCompletion": live_completion, "behaviors": behaviors, "language": language} - ), - auth=(self._username, self._password), - ) - if 200 <= _response.status_code < 300: - return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore - try: - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, body=_response.text) - raise ApiError(status_code=_response.status_code, body=_response_json) + """ + Set the user's aceConfig (the settings for the in-game Ace code editor), such as whether to enable autocomplete. - async def add_o_auth_identity( - self, - handle: str, - *, - provider: str, - access_token: typing.Optional[str] = None, - code: typing.Optional[str] = None, - ) -> UserResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "POST", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/o-auth-identities"), - json=jsonable_encoder({"provider": provider, "accessToken": access_token, "code": code}), - auth=(self._username, self._password), - ) + Parameters: + - handle: str. The document's `_id` or `slug`. + + - live_completion: typing.Optional[bool]. controls whether autocompletion snippets show up, the default value is true + + - behaviors: typing.Optional[bool]. controls whether things like automatic parenthesis and quote completion happens, the default value is false + + - language: typing.Optional[str]. only for home users, should be one of ["python", "javascript", "cpp", "lua", "coffeescript"] right now + """ + _request: typing.Dict[str, typing.Any] = {} + if live_completion is not OMIT: + _request["liveCompletion"] = live_completion + if behaviors is not OMIT: + _request["behaviors"] = behaviors + if language is not OMIT: + _request["language"] = language + _response = await self._client_wrapper.httpx_client.request( + "PUT", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/ace-config"), + json=jsonable_encoder(_request), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore try: @@ -396,14 +546,22 @@ async def add_o_auth_identity( raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def update_subscription(self, handle: str, *, ends: dt.datetime) -> UserResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/subscription"), - json=jsonable_encoder({"ends": ends}), - auth=(self._username, self._password), - ) + async def grant_premium_subscription(self, handle: str, *, ends: DatetimeString) -> UserResponse: + """ + Grants a user premium access to the "Home" version up to a certain time. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - ends: DatetimeString. + """ + _response = await self._client_wrapper.httpx_client.request( + "PUT", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/subscription"), + json=jsonable_encoder({"ends": ends}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore try: @@ -412,14 +570,22 @@ async def update_subscription(self, handle: str, *, ends: dt.datetime) -> UserRe raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def shorten_subscription(self, handle: str, *, ends: dt.datetime) -> UserResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/shorten-subscription"), - json=jsonable_encoder({"ends": ends}), - auth=(self._username, self._password), - ) + async def shorten_subscription(self, handle: str, *, ends: DatetimeString) -> UserResponse: + """ + If the user already has a premium access up to a certain time, this shortens/revokes his/her premium access. If the ends is less than or equal to the current time, it revokes the subscription and sets the end date to be the current time, else it just shortens the subscription. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - ends: DatetimeString. + """ + _response = await self._client_wrapper.httpx_client.request( + "PUT", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/shorten-subscription"), + json=jsonable_encoder({"ends": ends}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore try: @@ -428,14 +594,23 @@ async def shorten_subscription(self, handle: str, *, ends: dt.datetime) -> UserR raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def grant_license(self, handle: str, *, ends: dt.datetime) -> UserResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/license"), - json=jsonable_encoder({"ends": ends}), - auth=(self._username, self._password), - ) + async def grant_license(self, handle: str, *, ends: DatetimeString) -> UserResponse: + """ + Grants a user access to the "Classroom" version up to a certain time. + Sets their role to "student". + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - ends: DatetimeString. + """ + _response = await self._client_wrapper.httpx_client.request( + "PUT", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/license"), + json=jsonable_encoder({"ends": ends}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore try: @@ -444,14 +619,22 @@ async def grant_license(self, handle: str, *, ends: dt.datetime) -> UserResponse raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def shorten_license(self, handle: str, *, ends: dt.datetime) -> UserResponse: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "PUT", - urllib.parse.urljoin(f"{self._environment.value}/", f"users/{handle}/shorten-license"), - json=jsonable_encoder({"ends": ends}), - auth=(self._username, self._password), - ) + async def shorten_license(self, handle: str, *, ends: DatetimeString) -> UserResponse: + """ + If the user already has access to the "Classroom" version up to a certain time, this shortens/revokes his/her access. If the ends is less than or equal to the current time, it revokes the enrollment and sets the end date to be the current time, else it just shortens the enrollment. + + Parameters: + - handle: str. The document's `_id` or `slug`. + + - ends: DatetimeString. + """ + _response = await self._client_wrapper.httpx_client.request( + "PUT", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/shorten-license"), + json=jsonable_encoder({"ends": ends}), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore try: @@ -460,13 +643,21 @@ async def shorten_license(self, handle: str, *, ends: dt.datetime) -> UserRespon raise ApiError(status_code=_response.status_code, body=_response.text) raise ApiError(status_code=_response.status_code, body=_response_json) - async def find_user(self, property: str, value: str) -> None: - async with httpx.AsyncClient() as _client: - _response = await _client.request( - "GET", - urllib.parse.urljoin(f"{self._environment.value}/", f"user-lookup/{property}/{value}"), - auth=(self._username, self._password), - ) + async def lookup(self, property: str, value: str) -> None: + """ + Redirects to `/users/{handle}` given a unique, identifying property + + Parameters: + - property: str. The property to lookup by. May either be `"israel-id"` or `"name"`. + + - value: str. The value to be looked up. + """ + _response = await self._client_wrapper.httpx_client.request( + "GET", + urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"user-lookup/{property}/{value}"), + headers=self._client_wrapper.get_headers(), + timeout=60, + ) if 200 <= _response.status_code < 300: return try: diff --git a/src/codecombat/resources/users/types/__init__.py b/src/codecombat/resources/users/types/__init__.py deleted file mode 100644 index 32bae44..0000000 --- a/src/codecombat/resources/users/types/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from .hero_config import HeroConfig -from .user_role import UserRole - -__all__ = ["HeroConfig", "UserRole"] diff --git a/src/codecombat/types/__init__.py b/src/codecombat/types/__init__.py new file mode 100644 index 0000000..40511f5 --- /dev/null +++ b/src/codecombat/types/__init__.py @@ -0,0 +1,51 @@ +# This file was auto-generated by Fern from our API Definition. + +from .clan_response import ClanResponse +from .classroom_response import ClassroomResponse +from .classroom_response_courses_item import ClassroomResponseCoursesItem +from .classroom_response_with_code import ClassroomResponseWithCode +from .classroom_response_with_code_courses_item import ClassroomResponseWithCodeCoursesItem +from .classrooms_create_request_ace_config import ClassroomsCreateRequestAceConfig +from .classrooms_get_members_stats_response_item import ClassroomsGetMembersStatsResponseItem +from .classrooms_get_members_stats_response_item_stats import ClassroomsGetMembersStatsResponseItemStats +from .datetime_string import DatetimeString +from .level_session_response import LevelSessionResponse +from .level_session_response_level import LevelSessionResponseLevel +from .level_session_response_state import LevelSessionResponseState +from .license_stats_response import LicenseStatsResponse +from .object_id_string import ObjectIdString +from .playtime_stats_response import PlaytimeStatsResponse +from .role_string import RoleString +from .user_response import UserResponse +from .user_response_license import UserResponseLicense +from .user_response_o_auth_identities_item import UserResponseOAuthIdentitiesItem +from .user_response_stats import UserResponseStats +from .user_response_subscription import UserResponseSubscription +from .users_create_request_hero_config import UsersCreateRequestHeroConfig +from .users_create_request_role import UsersCreateRequestRole + +__all__ = [ + "ClanResponse", + "ClassroomResponse", + "ClassroomResponseCoursesItem", + "ClassroomResponseWithCode", + "ClassroomResponseWithCodeCoursesItem", + "ClassroomsCreateRequestAceConfig", + "ClassroomsGetMembersStatsResponseItem", + "ClassroomsGetMembersStatsResponseItemStats", + "DatetimeString", + "LevelSessionResponse", + "LevelSessionResponseLevel", + "LevelSessionResponseState", + "LicenseStatsResponse", + "ObjectIdString", + "PlaytimeStatsResponse", + "RoleString", + "UserResponse", + "UserResponseLicense", + "UserResponseOAuthIdentitiesItem", + "UserResponseStats", + "UserResponseSubscription", + "UsersCreateRequestHeroConfig", + "UsersCreateRequestRole", +] diff --git a/src/codecombat/resources/clans/types/clan_response.py b/src/codecombat/types/clan_response.py similarity index 90% rename from src/codecombat/resources/clans/types/clan_response.py rename to src/codecombat/types/clan_response.py index ad24e4f..4a65bf4 100644 --- a/src/codecombat/resources/clans/types/clan_response.py +++ b/src/codecombat/types/clan_response.py @@ -5,8 +5,8 @@ import pydantic -from ....core.datetime_utils import serialize_datetime -from ...commons.types.object_id_string import ObjectIdString +from ..core.datetime_utils import serialize_datetime +from .object_id_string import ObjectIdString class ClanResponse(pydantic.BaseModel): @@ -34,5 +34,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/commons/types/classroom_response.py b/src/codecombat/types/classroom_response.py similarity index 63% rename from src/codecombat/resources/commons/types/classroom_response.py rename to src/codecombat/types/classroom_response.py index a9b1d75..db78bbc 100644 --- a/src/codecombat/resources/commons/types/classroom_response.py +++ b/src/codecombat/types/classroom_response.py @@ -5,8 +5,8 @@ import pydantic -from ....core.datetime_utils import serialize_datetime -from .course import Course +from ..core.datetime_utils import serialize_datetime +from .classroom_response_courses_item import ClassroomResponseCoursesItem from .object_id_string import ObjectIdString @@ -16,15 +16,11 @@ class ClassroomResponse(pydantic.BaseModel): """ id: typing.Optional[ObjectIdString] = pydantic.Field(alias="_id") - name: typing.Optional[str] = pydantic.Field(description=("The name of the classroom\n")) - members: typing.Optional[typing.List[ObjectIdString]] = pydantic.Field( - description=("List of _ids of the student members of the classroom\n") - ) - owner_id: typing.Optional[ObjectIdString] = pydantic.Field( - alias="ownerID", description=("The _id of the teacher owner of the classroom.\n") - ) + name: typing.Optional[str] + members: typing.Optional[typing.List[ObjectIdString]] + owner_id: typing.Optional[ObjectIdString] = pydantic.Field(alias="ownerID") description: typing.Optional[str] - courses: typing.Optional[typing.List[Course]] + courses: typing.Optional[typing.List[ClassroomResponseCoursesItem]] def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} @@ -36,5 +32,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/commons/types/course.py b/src/codecombat/types/classroom_response_courses_item.py similarity index 87% rename from src/codecombat/resources/commons/types/course.py rename to src/codecombat/types/classroom_response_courses_item.py index 75c5b4a..504f5e7 100644 --- a/src/codecombat/resources/commons/types/course.py +++ b/src/codecombat/types/classroom_response_courses_item.py @@ -5,11 +5,11 @@ import pydantic -from ....core.datetime_utils import serialize_datetime +from ..core.datetime_utils import serialize_datetime from .object_id_string import ObjectIdString -class Course(pydantic.BaseModel): +class ClassroomResponseCoursesItem(pydantic.BaseModel): id: typing.Optional[ObjectIdString] = pydantic.Field(alias="_id") levels: typing.Optional[typing.List[typing.Dict[str, typing.Any]]] enrolled: typing.Optional[typing.List[ObjectIdString]] @@ -25,5 +25,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/commons/types/classroom_response_with_code.py b/src/codecombat/types/classroom_response_with_code.py similarity index 83% rename from src/codecombat/resources/commons/types/classroom_response_with_code.py rename to src/codecombat/types/classroom_response_with_code.py index e76de82..ab7ead4 100644 --- a/src/codecombat/resources/commons/types/classroom_response_with_code.py +++ b/src/codecombat/types/classroom_response_with_code.py @@ -5,8 +5,8 @@ import pydantic -from ....core.datetime_utils import serialize_datetime -from .course import Course +from ..core.datetime_utils import serialize_datetime +from .classroom_response_with_code_courses_item import ClassroomResponseWithCodeCoursesItem from .object_id_string import ObjectIdString @@ -22,7 +22,7 @@ class ClassroomResponseWithCode(pydantic.BaseModel): description: typing.Optional[str] code: typing.Optional[str] code_camel: typing.Optional[str] = pydantic.Field(alias="codeCamel") - courses: typing.Optional[typing.List[Course]] + courses: typing.Optional[typing.List[ClassroomResponseWithCodeCoursesItem]] clan_id: typing.Optional[ObjectIdString] = pydantic.Field(alias="clanId") def json(self, **kwargs: typing.Any) -> str: @@ -35,5 +35,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/types/classroom_response_with_code_courses_item.py b/src/codecombat/types/classroom_response_with_code_courses_item.py new file mode 100644 index 0000000..db3cd42 --- /dev/null +++ b/src/codecombat/types/classroom_response_with_code_courses_item.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic + +from ..core.datetime_utils import serialize_datetime +from .object_id_string import ObjectIdString + + +class ClassroomResponseWithCodeCoursesItem(pydantic.BaseModel): + id: typing.Optional[ObjectIdString] = pydantic.Field(alias="_id") + levels: typing.Optional[typing.List[typing.Dict[str, typing.Any]]] + enrolled: typing.Optional[typing.List[ObjectIdString]] + instance_id: typing.Optional[ObjectIdString] + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + return super().dict(**kwargs_with_defaults) + + class Config: + frozen = True + smart_union = True + allow_population_by_field_name = True + json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/classrooms/types/ace_config.py b/src/codecombat/types/classrooms_create_request_ace_config.py similarity index 79% rename from src/codecombat/resources/classrooms/types/ace_config.py rename to src/codecombat/types/classrooms_create_request_ace_config.py index abbd841..0aa33f0 100644 --- a/src/codecombat/resources/classrooms/types/ace_config.py +++ b/src/codecombat/types/classrooms_create_request_ace_config.py @@ -5,11 +5,11 @@ import pydantic -from ....core.datetime_utils import serialize_datetime +from ..core.datetime_utils import serialize_datetime -class AceConfig(pydantic.BaseModel): - language: typing.Optional[str] = pydantic.Field(description=("Programming language for the classroom\n")) +class ClassroomsCreateRequestAceConfig(pydantic.BaseModel): + language: typing.Optional[str] = pydantic.Field(description="Programming language for the classroom") def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} @@ -21,4 +21,5 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/classrooms/types/member_stat.py b/src/codecombat/types/classrooms_get_members_stats_response_item.py similarity index 67% rename from src/codecombat/resources/classrooms/types/member_stat.py rename to src/codecombat/types/classrooms_get_members_stats_response_item.py index 0284dd1..d793b88 100644 --- a/src/codecombat/resources/classrooms/types/member_stat.py +++ b/src/codecombat/types/classrooms_get_members_stats_response_item.py @@ -5,14 +5,14 @@ import pydantic -from ....core.datetime_utils import serialize_datetime -from ...commons.types.object_id_string import ObjectIdString -from .play_stats import PlayStats +from ..core.datetime_utils import serialize_datetime +from .classrooms_get_members_stats_response_item_stats import ClassroomsGetMembersStatsResponseItemStats +from .object_id_string import ObjectIdString -class MemberStat(pydantic.BaseModel): +class ClassroomsGetMembersStatsResponseItem(pydantic.BaseModel): id: typing.Optional[ObjectIdString] = pydantic.Field(alias="_id") - stats: typing.Optional[PlayStats] + stats: typing.Optional[ClassroomsGetMembersStatsResponseItemStats] def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} @@ -24,5 +24,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/classrooms/types/play_stats.py b/src/codecombat/types/classrooms_get_members_stats_response_item_stats.py similarity index 82% rename from src/codecombat/resources/classrooms/types/play_stats.py rename to src/codecombat/types/classrooms_get_members_stats_response_item_stats.py index d4ab588..6b89ba5 100644 --- a/src/codecombat/resources/classrooms/types/play_stats.py +++ b/src/codecombat/types/classrooms_get_members_stats_response_item_stats.py @@ -5,12 +5,12 @@ import pydantic -from ....core.datetime_utils import serialize_datetime +from ..core.datetime_utils import serialize_datetime -class PlayStats(pydantic.BaseModel): +class ClassroomsGetMembersStatsResponseItemStats(pydantic.BaseModel): games_completed: typing.Optional[float] = pydantic.Field(alias="gamesCompleted") - playtime: typing.Optional[float] = pydantic.Field(description=("Total play time in seconds\n")) + playtime: typing.Optional[float] = pydantic.Field(description="Total play time in seconds") def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} @@ -22,5 +22,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/commons/types/datetime_string.py b/src/codecombat/types/datetime_string.py similarity index 100% rename from src/codecombat/resources/commons/types/datetime_string.py rename to src/codecombat/types/datetime_string.py diff --git a/src/codecombat/resources/classrooms/types/level_session_response.py b/src/codecombat/types/level_session_response.py similarity index 64% rename from src/codecombat/resources/classrooms/types/level_session_response.py rename to src/codecombat/types/level_session_response.py index 4fb2514..9c4a463 100644 --- a/src/codecombat/resources/classrooms/types/level_session_response.py +++ b/src/codecombat/types/level_session_response.py @@ -5,27 +5,27 @@ import pydantic -from ....core.datetime_utils import serialize_datetime -from ...commons.types.datetime_string import DatetimeString -from ...commons.types.object_id_string import ObjectIdString -from .level import Level -from .state import State +from ..core.datetime_utils import serialize_datetime +from .datetime_string import DatetimeString +from .level_session_response_level import LevelSessionResponseLevel +from .level_session_response_state import LevelSessionResponseState +from .object_id_string import ObjectIdString class LevelSessionResponse(pydantic.BaseModel): - state: typing.Optional[State] - level: typing.Optional[Level] - level_id: typing.Optional[str] = pydantic.Field(alias="levelID", description=("Level slug like `wakka-maul`\n")) + state: typing.Optional[LevelSessionResponseState] + level: typing.Optional[LevelSessionResponseLevel] + level_id: typing.Optional[str] = pydantic.Field(alias="levelID", description="Level slug like `wakka-maul`") creator: typing.Optional[ObjectIdString] - playtime: typing.Optional[int] = pydantic.Field(description=("Time played in seconds.\n")) + playtime: typing.Optional[int] = pydantic.Field(description="Time played in seconds.") changed: typing.Optional[DatetimeString] created: typing.Optional[DatetimeString] date_first_completed: typing.Optional[DatetimeString] = pydantic.Field(alias="dateFirstCompleted") submitted: typing.Optional[bool] = pydantic.Field( - description=("For arenas. Whether or not the level has been added to the ladder.\n") + description="For arenas. Whether or not the level has been added to the ladder." ) published: typing.Optional[bool] = pydantic.Field( - description=("For shareable projects. Whether or not the project has been shared with classmates.\n") + description="For shareable projects. Whether or not the project has been shared with classmates." ) def json(self, **kwargs: typing.Any) -> str: @@ -38,5 +38,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/classrooms/types/level.py b/src/codecombat/types/level_session_response_level.py similarity index 81% rename from src/codecombat/resources/classrooms/types/level.py rename to src/codecombat/types/level_session_response_level.py index cd98f00..f6cb79d 100644 --- a/src/codecombat/resources/classrooms/types/level.py +++ b/src/codecombat/types/level_session_response_level.py @@ -5,11 +5,11 @@ import pydantic -from ....core.datetime_utils import serialize_datetime +from ..core.datetime_utils import serialize_datetime -class Level(pydantic.BaseModel): - original: typing.Optional[str] = pydantic.Field(description=("The id for the level.\n")) +class LevelSessionResponseLevel(pydantic.BaseModel): + original: typing.Optional[str] = pydantic.Field(description="The id for the level.") def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} @@ -21,4 +21,5 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/classrooms/types/state.py b/src/codecombat/types/level_session_response_state.py similarity index 83% rename from src/codecombat/resources/classrooms/types/state.py rename to src/codecombat/types/level_session_response_state.py index 9b87048..b1601db 100644 --- a/src/codecombat/resources/classrooms/types/state.py +++ b/src/codecombat/types/level_session_response_state.py @@ -5,10 +5,10 @@ import pydantic -from ....core.datetime_utils import serialize_datetime +from ..core.datetime_utils import serialize_datetime -class State(pydantic.BaseModel): +class LevelSessionResponseState(pydantic.BaseModel): complete: typing.Optional[bool] def json(self, **kwargs: typing.Any) -> str: @@ -21,4 +21,5 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/stats/types/license_stats_response.py b/src/codecombat/types/license_stats_response.py similarity index 71% rename from src/codecombat/resources/stats/types/license_stats_response.py rename to src/codecombat/types/license_stats_response.py index d6c89d7..e574ad8 100644 --- a/src/codecombat/resources/stats/types/license_stats_response.py +++ b/src/codecombat/types/license_stats_response.py @@ -5,21 +5,21 @@ import pydantic -from ....core.datetime_utils import serialize_datetime +from ..core.datetime_utils import serialize_datetime class LicenseStatsResponse(pydantic.BaseModel): license_days_granted: typing.Optional[float] = pydantic.Field( - alias="licenseDaysGranted", description=("Total number of license days granted\n") + alias="licenseDaysGranted", description="Total number of license days granted" ) license_days_used: typing.Optional[float] = pydantic.Field( - alias="licenseDaysUsed", description=("Number of license days used\n") + alias="licenseDaysUsed", description="Number of license days used" ) license_days_remaining: typing.Optional[float] = pydantic.Field( - alias="licenseDaysRemaining", description=("Number of license days remaining\n") + alias="licenseDaysRemaining", description="Number of license days remaining" ) active_licenses: typing.Optional[float] = pydantic.Field( - alias="activeLicenses", description=("Number of active/valid licenses\n") + alias="activeLicenses", description="Number of active/valid licenses" ) def json(self, **kwargs: typing.Any) -> str: @@ -32,5 +32,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/commons/types/object_id_string.py b/src/codecombat/types/object_id_string.py similarity index 100% rename from src/codecombat/resources/commons/types/object_id_string.py rename to src/codecombat/types/object_id_string.py diff --git a/src/codecombat/resources/stats/types/playtime_stats_response.py b/src/codecombat/types/playtime_stats_response.py similarity index 76% rename from src/codecombat/resources/stats/types/playtime_stats_response.py rename to src/codecombat/types/playtime_stats_response.py index 62de36a..3083348 100644 --- a/src/codecombat/resources/stats/types/playtime_stats_response.py +++ b/src/codecombat/types/playtime_stats_response.py @@ -5,14 +5,12 @@ import pydantic -from ....core.datetime_utils import serialize_datetime +from ..core.datetime_utils import serialize_datetime class PlaytimeStatsResponse(pydantic.BaseModel): - play_time: typing.Optional[float] = pydantic.Field(alias="playTime", description=("Total play time in seconds\n")) - games_played: typing.Optional[float] = pydantic.Field( - alias="gamesPlayed", description=("Number of levels played\n") - ) + play_time: typing.Optional[float] = pydantic.Field(alias="playTime", description="Total play time in seconds") + games_played: typing.Optional[float] = pydantic.Field(alias="gamesPlayed", description="Number of levels played") def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} @@ -24,5 +22,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/commons/types/role_string.py b/src/codecombat/types/role_string.py similarity index 100% rename from src/codecombat/resources/commons/types/role_string.py rename to src/codecombat/types/role_string.py diff --git a/src/codecombat/resources/commons/types/user_response.py b/src/codecombat/types/user_response.py similarity index 62% rename from src/codecombat/resources/commons/types/user_response.py rename to src/codecombat/types/user_response.py index d7dc984..d64070b 100644 --- a/src/codecombat/resources/commons/types/user_response.py +++ b/src/codecombat/types/user_response.py @@ -5,13 +5,13 @@ import pydantic -from ....core.datetime_utils import serialize_datetime -from .auth_identity import AuthIdentity -from .license import License +from ..core.datetime_utils import serialize_datetime from .object_id_string import ObjectIdString from .role_string import RoleString -from .subscription import Subscription -from .user_stats import UserStats +from .user_response_license import UserResponseLicense +from .user_response_o_auth_identities_item import UserResponseOAuthIdentitiesItem +from .user_response_stats import UserResponseStats +from .user_response_subscription import UserResponseSubscription class UserResponse(pydantic.BaseModel): @@ -24,10 +24,12 @@ class UserResponse(pydantic.BaseModel): name: typing.Optional[str] slug: typing.Optional[str] role: typing.Optional[RoleString] - stats: typing.Optional[UserStats] - o_auth_identities: typing.Optional[typing.List[AuthIdentity]] = pydantic.Field(alias="oAuthIdentities") - subscription: typing.Optional[Subscription] - license: typing.Optional[License] + stats: typing.Optional[UserResponseStats] + o_auth_identities: typing.Optional[typing.List[UserResponseOAuthIdentitiesItem]] = pydantic.Field( + alias="oAuthIdentities" + ) + subscription: typing.Optional[UserResponseSubscription] + license: typing.Optional[UserResponseLicense] def json(self, **kwargs: typing.Any) -> str: kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} @@ -39,5 +41,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/commons/types/license.py b/src/codecombat/types/user_response_license.py similarity index 85% rename from src/codecombat/resources/commons/types/license.py rename to src/codecombat/types/user_response_license.py index f82d65b..63d082d 100644 --- a/src/codecombat/resources/commons/types/license.py +++ b/src/codecombat/types/user_response_license.py @@ -5,11 +5,11 @@ import pydantic -from ....core.datetime_utils import serialize_datetime +from ..core.datetime_utils import serialize_datetime from .datetime_string import DatetimeString -class License(pydantic.BaseModel): +class UserResponseLicense(pydantic.BaseModel): ends: typing.Optional[DatetimeString] active: typing.Optional[bool] @@ -23,4 +23,5 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/commons/types/auth_identity.py b/src/codecombat/types/user_response_o_auth_identities_item.py similarity index 83% rename from src/codecombat/resources/commons/types/auth_identity.py rename to src/codecombat/types/user_response_o_auth_identities_item.py index 35dba4d..9eefe4e 100644 --- a/src/codecombat/resources/commons/types/auth_identity.py +++ b/src/codecombat/types/user_response_o_auth_identities_item.py @@ -5,10 +5,10 @@ import pydantic -from ....core.datetime_utils import serialize_datetime +from ..core.datetime_utils import serialize_datetime -class AuthIdentity(pydantic.BaseModel): +class UserResponseOAuthIdentitiesItem(pydantic.BaseModel): provider: typing.Optional[str] id: typing.Optional[str] @@ -22,4 +22,5 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/commons/types/user_stats.py b/src/codecombat/types/user_response_stats.py similarity index 79% rename from src/codecombat/resources/commons/types/user_stats.py rename to src/codecombat/types/user_response_stats.py index 0bf744f..59d68f9 100644 --- a/src/codecombat/resources/commons/types/user_stats.py +++ b/src/codecombat/types/user_response_stats.py @@ -5,14 +5,14 @@ import pydantic -from ....core.datetime_utils import serialize_datetime +from ..core.datetime_utils import serialize_datetime -class UserStats(pydantic.BaseModel): +class UserResponseStats(pydantic.BaseModel): games_completed: typing.Optional[float] = pydantic.Field(alias="gamesCompleted") concepts: typing.Optional[typing.Dict[str, float]] play_time: typing.Optional[float] = pydantic.Field( - alias="playTime", description=("Included only when specifically requested on the endpoint\n") + alias="playTime", description="Included only when specifically requested on the endpoint" ) def json(self, **kwargs: typing.Any) -> str: @@ -25,5 +25,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/commons/types/subscription.py b/src/codecombat/types/user_response_subscription.py similarity index 85% rename from src/codecombat/resources/commons/types/subscription.py rename to src/codecombat/types/user_response_subscription.py index dc86084..9e0a5c5 100644 --- a/src/codecombat/resources/commons/types/subscription.py +++ b/src/codecombat/types/user_response_subscription.py @@ -5,11 +5,11 @@ import pydantic -from ....core.datetime_utils import serialize_datetime +from ..core.datetime_utils import serialize_datetime from .datetime_string import DatetimeString -class Subscription(pydantic.BaseModel): +class UserResponseSubscription(pydantic.BaseModel): ends: typing.Optional[DatetimeString] active: typing.Optional[bool] @@ -23,4 +23,5 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/users/types/hero_config.py b/src/codecombat/types/users_create_request_hero_config.py similarity index 80% rename from src/codecombat/resources/users/types/hero_config.py rename to src/codecombat/types/users_create_request_hero_config.py index 0fd45d4..b9efe49 100644 --- a/src/codecombat/resources/users/types/hero_config.py +++ b/src/codecombat/types/users_create_request_hero_config.py @@ -5,11 +5,11 @@ import pydantic -from ....core.datetime_utils import serialize_datetime -from ...commons.types.object_id_string import ObjectIdString +from ..core.datetime_utils import serialize_datetime +from .object_id_string import ObjectIdString -class HeroConfig(pydantic.BaseModel): +class UsersCreateRequestHeroConfig(pydantic.BaseModel): thang_type: typing.Optional[ObjectIdString] = pydantic.Field(alias="thangType") def json(self, **kwargs: typing.Any) -> str: @@ -22,5 +22,6 @@ def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: class Config: frozen = True + smart_union = True allow_population_by_field_name = True json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/codecombat/resources/users/types/user_role.py b/src/codecombat/types/users_create_request_role.py similarity index 60% rename from src/codecombat/resources/users/types/user_role.py rename to src/codecombat/types/users_create_request_role.py index 80d6899..e0fedfb 100644 --- a/src/codecombat/resources/users/types/user_role.py +++ b/src/codecombat/types/users_create_request_role.py @@ -6,16 +6,16 @@ T_Result = typing.TypeVar("T_Result") -class UserRole(str, enum.Enum): +class UsersCreateRequestRole(str, enum.Enum): """ - A `"student"` or `"teacher"`. If unset, a home user will be created, unable to join classrooms. + `"student"` or `"teacher"`. If unset, a home user will be created, unable to join classrooms. """ STUDENT = "student" TEACHER = "teacher" def visit(self, student: typing.Callable[[], T_Result], teacher: typing.Callable[[], T_Result]) -> T_Result: - if self is UserRole.STUDENT: + if self is UsersCreateRequestRole.STUDENT: return student() - if self is UserRole.TEACHER: + if self is UsersCreateRequestRole.TEACHER: return teacher() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..60a58e6 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,6 @@ +import pytest + +# Get started with writing tests with pytest at https://docs.pytest.org +@pytest.mark.skip(reason="Unimplemented") +def test_client() -> None: + assert True == True