Skip to content

Commit 200971a

Browse files
committed
Release 0.1.7
1 parent d3ee3e4 commit 200971a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1387
-796
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: ci
33
on: [push]
44
jobs:
55
compile:
6-
runs-on: ubuntu-latest
6+
runs-on: ubuntu-20.04
77
steps:
88
- name: Checkout repo
99
uses: actions/checkout@v3
@@ -13,17 +13,32 @@ jobs:
1313
python-version: 3.7
1414
- name: Bootstrap poetry
1515
run: |
16-
curl -sSL https://install.python-poetry.org | python - -y
16+
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
1717
- name: Install dependencies
1818
run: poetry install
1919
- name: Compile
2020
run: poetry run mypy .
21+
test:
22+
runs-on: ubuntu-20.04
23+
steps:
24+
- name: Checkout repo
25+
uses: actions/checkout@v3
26+
- name: Set up python
27+
uses: actions/setup-python@v4
28+
with:
29+
python-version: 3.7
30+
- name: Bootstrap poetry
31+
run: |
32+
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
33+
- name: Install dependencies
34+
run: poetry install
35+
- name: Test
36+
run: poetry run pytest .
2137

2238
publish:
23-
needs: [ compile ]
39+
needs: [compile, test]
2440
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
25-
runs-on: ubuntu-latest
26-
41+
runs-on: ubuntu-20.04
2742
steps:
2843
- name: Checkout repo
2944
uses: actions/checkout@v3
@@ -33,7 +48,7 @@ jobs:
3348
python-version: 3.7
3449
- name: Bootstrap poetry
3550
run: |
36-
curl -sSL https://install.python-poetry.org | python - -y
51+
curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1
3752
- name: Install dependencies
3853
run: poetry install
3954
- name: Publish to pypi
@@ -42,4 +57,4 @@ jobs:
4257
poetry --no-interaction -v publish --build --repository remote --username "$PYPI_USERNAME" --password "$PYPI_PASSWORD"
4358
env:
4459
PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }}
45-
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
60+
PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}

pyproject.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
1-
21
[tool.poetry]
32
name = "codecombat"
4-
version = "0.0.18"
3+
version = "0.1.7"
54
description = ""
5+
readme = "README.md"
66
authors = []
77
packages = [
88
{ include = "codecombat", from = "src"}
99
]
1010

1111
[tool.poetry.dependencies]
1212
python = "^3.7"
13+
httpx = ">=0.21.2"
1314
pydantic = "^1.9.2"
14-
httpx = "0.23.3"
15-
types-backports = "0.1.3"
16-
backports-cached_property = "1.0.2"
1715

1816
[tool.poetry.dev-dependencies]
1917
mypy = "0.971"
18+
pytest = "^7.4.0"
2019

2120
[build-system]
2221
requires = ["poetry-core"]

src/codecombat/__init__.py

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,61 @@
11
# This file was auto-generated by Fern from our API Definition.
22

3-
from .environment import CodeCombatEnvironment
4-
from .resources import (
5-
AceConfig,
6-
AuthIdentity,
3+
from .types import (
74
ClanResponse,
85
ClassroomResponse,
6+
ClassroomResponseCoursesItem,
97
ClassroomResponseWithCode,
10-
Course,
8+
ClassroomResponseWithCodeCoursesItem,
9+
ClassroomsCreateRequestAceConfig,
10+
ClassroomsGetMembersStatsResponseItem,
11+
ClassroomsGetMembersStatsResponseItemStats,
1112
DatetimeString,
12-
HeroConfig,
13-
Level,
1413
LevelSessionResponse,
15-
License,
14+
LevelSessionResponseLevel,
15+
LevelSessionResponseState,
1616
LicenseStatsResponse,
17-
MemberStat,
1817
ObjectIdString,
19-
PlayStats,
2018
PlaytimeStatsResponse,
2119
RoleString,
22-
State,
23-
Subscription,
2420
UserResponse,
25-
UserRole,
26-
UserStats,
27-
auth,
28-
clans,
29-
classrooms,
30-
commons,
31-
stats,
32-
users,
21+
UserResponseLicense,
22+
UserResponseOAuthIdentitiesItem,
23+
UserResponseStats,
24+
UserResponseSubscription,
25+
UsersCreateRequestHeroConfig,
26+
UsersCreateRequestRole,
3327
)
28+
from .resources import auth, clans, classrooms, stats, users
29+
from .environment import CodeCombatEnvironment
3430

3531
__all__ = [
36-
"AceConfig",
37-
"AuthIdentity",
3832
"ClanResponse",
3933
"ClassroomResponse",
34+
"ClassroomResponseCoursesItem",
4035
"ClassroomResponseWithCode",
36+
"ClassroomResponseWithCodeCoursesItem",
37+
"ClassroomsCreateRequestAceConfig",
38+
"ClassroomsGetMembersStatsResponseItem",
39+
"ClassroomsGetMembersStatsResponseItemStats",
4140
"CodeCombatEnvironment",
42-
"Course",
4341
"DatetimeString",
44-
"HeroConfig",
45-
"Level",
4642
"LevelSessionResponse",
47-
"License",
43+
"LevelSessionResponseLevel",
44+
"LevelSessionResponseState",
4845
"LicenseStatsResponse",
49-
"MemberStat",
5046
"ObjectIdString",
51-
"PlayStats",
5247
"PlaytimeStatsResponse",
5348
"RoleString",
54-
"State",
55-
"Subscription",
5649
"UserResponse",
57-
"UserRole",
58-
"UserStats",
50+
"UserResponseLicense",
51+
"UserResponseOAuthIdentitiesItem",
52+
"UserResponseStats",
53+
"UserResponseSubscription",
54+
"UsersCreateRequestHeroConfig",
55+
"UsersCreateRequestRole",
5956
"auth",
6057
"clans",
6158
"classrooms",
62-
"commons",
6359
"stats",
6460
"users",
6561
]

src/codecombat/client.py

Lines changed: 138 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,157 @@
11
# This file was auto-generated by Fern from our API Definition.
22

3-
from backports.cached_property import cached_property
3+
import typing
4+
import urllib.parse
5+
from json.decoder import JSONDecodeError
46

7+
import httpx
8+
import pydantic
9+
10+
from .core.api_error import ApiError
11+
from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
12+
from .core.jsonable_encoder import jsonable_encoder
513
from .environment import CodeCombatEnvironment
614
from .resources.auth.client import AsyncAuthClient, AuthClient
715
from .resources.clans.client import AsyncClansClient, ClansClient
816
from .resources.classrooms.client import AsyncClassroomsClient, ClassroomsClient
917
from .resources.stats.client import AsyncStatsClient, StatsClient
1018
from .resources.users.client import AsyncUsersClient, UsersClient
19+
from .types.user_response import UserResponse
20+
21+
# this is used as the default value for optional parameters
22+
OMIT = typing.cast(typing.Any, ...)
1123

1224

1325
class CodeCombat:
1426
def __init__(
15-
self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str
27+
self,
28+
*,
29+
base_url: typing.Optional[str] = None,
30+
environment: CodeCombatEnvironment = CodeCombatEnvironment.DEFAULT,
31+
username: typing.Union[str, typing.Callable[[], str]],
32+
password: typing.Union[str, typing.Callable[[], str]],
33+
timeout: typing.Optional[float] = 60,
1634
):
17-
self._environment = environment
18-
self._username = username
19-
self._password = password
20-
21-
@cached_property
22-
def auth(self) -> AuthClient:
23-
return AuthClient(environment=self._environment, username=self._username, password=self._password)
24-
25-
@cached_property
26-
def clans(self) -> ClansClient:
27-
return ClansClient(environment=self._environment, username=self._username, password=self._password)
28-
29-
@cached_property
30-
def classrooms(self) -> ClassroomsClient:
31-
return ClassroomsClient(environment=self._environment, username=self._username, password=self._password)
32-
33-
@cached_property
34-
def stats(self) -> StatsClient:
35-
return StatsClient(environment=self._environment, username=self._username, password=self._password)
36-
37-
@cached_property
38-
def users(self) -> UsersClient:
39-
return UsersClient(environment=self._environment, username=self._username, password=self._password)
35+
self._client_wrapper = SyncClientWrapper(
36+
base_url=_get_base_url(base_url=base_url, environment=environment),
37+
username=username,
38+
password=password,
39+
httpx_client=httpx.Client(timeout=timeout),
40+
)
41+
self.auth = AuthClient(client_wrapper=self._client_wrapper)
42+
self.clans = ClansClient(client_wrapper=self._client_wrapper)
43+
self.classrooms = ClassroomsClient(client_wrapper=self._client_wrapper)
44+
self.stats = StatsClient(client_wrapper=self._client_wrapper)
45+
self.users = UsersClient(client_wrapper=self._client_wrapper)
46+
47+
def post_users_handle_o_auth_identities(
48+
self,
49+
handle: str,
50+
*,
51+
provider: str,
52+
access_token: typing.Optional[str] = OMIT,
53+
code: typing.Optional[str] = OMIT,
54+
) -> UserResponse:
55+
"""
56+
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.
57+
58+
Parameters:
59+
- handle: str. The document's `_id` or `slug`.
60+
61+
- provider: str. Your OAuth Provider ID.
62+
63+
- access_token: typing.Optional[str]. Will be passed through your lookup URL to get the user ID. Required if no `code`.
64+
65+
- code: typing.Optional[str]. Will be passed to the OAuth token endpoint to get a token. Required if no `accessToken`.
66+
"""
67+
_request: typing.Dict[str, typing.Any] = {"provider": provider}
68+
if access_token is not OMIT:
69+
_request["accessToken"] = access_token
70+
if code is not OMIT:
71+
_request["code"] = code
72+
_response = self._client_wrapper.httpx_client.request(
73+
"POST",
74+
urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/o-auth-identities"),
75+
json=jsonable_encoder(_request),
76+
headers=self._client_wrapper.get_headers(),
77+
timeout=60,
78+
)
79+
if 200 <= _response.status_code < 300:
80+
return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore
81+
try:
82+
_response_json = _response.json()
83+
except JSONDecodeError:
84+
raise ApiError(status_code=_response.status_code, body=_response.text)
85+
raise ApiError(status_code=_response.status_code, body=_response_json)
4086

4187

4288
class AsyncCodeCombat:
4389
def __init__(
44-
self, *, environment: CodeCombatEnvironment = CodeCombatEnvironment.PRODUCTION, username: str, password: str
90+
self,
91+
*,
92+
base_url: typing.Optional[str] = None,
93+
environment: CodeCombatEnvironment = CodeCombatEnvironment.DEFAULT,
94+
username: typing.Union[str, typing.Callable[[], str]],
95+
password: typing.Union[str, typing.Callable[[], str]],
96+
timeout: typing.Optional[float] = 60,
4597
):
46-
self._environment = environment
47-
self._username = username
48-
self._password = password
49-
50-
@cached_property
51-
def auth(self) -> AsyncAuthClient:
52-
return AsyncAuthClient(environment=self._environment, username=self._username, password=self._password)
53-
54-
@cached_property
55-
def clans(self) -> AsyncClansClient:
56-
return AsyncClansClient(environment=self._environment, username=self._username, password=self._password)
57-
58-
@cached_property
59-
def classrooms(self) -> AsyncClassroomsClient:
60-
return AsyncClassroomsClient(environment=self._environment, username=self._username, password=self._password)
61-
62-
@cached_property
63-
def stats(self) -> AsyncStatsClient:
64-
return AsyncStatsClient(environment=self._environment, username=self._username, password=self._password)
65-
66-
@cached_property
67-
def users(self) -> AsyncUsersClient:
68-
return AsyncUsersClient(environment=self._environment, username=self._username, password=self._password)
98+
self._client_wrapper = AsyncClientWrapper(
99+
base_url=_get_base_url(base_url=base_url, environment=environment),
100+
username=username,
101+
password=password,
102+
httpx_client=httpx.AsyncClient(timeout=timeout),
103+
)
104+
self.auth = AsyncAuthClient(client_wrapper=self._client_wrapper)
105+
self.clans = AsyncClansClient(client_wrapper=self._client_wrapper)
106+
self.classrooms = AsyncClassroomsClient(client_wrapper=self._client_wrapper)
107+
self.stats = AsyncStatsClient(client_wrapper=self._client_wrapper)
108+
self.users = AsyncUsersClient(client_wrapper=self._client_wrapper)
109+
110+
async def post_users_handle_o_auth_identities(
111+
self,
112+
handle: str,
113+
*,
114+
provider: str,
115+
access_token: typing.Optional[str] = OMIT,
116+
code: typing.Optional[str] = OMIT,
117+
) -> UserResponse:
118+
"""
119+
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.
120+
121+
Parameters:
122+
- handle: str. The document's `_id` or `slug`.
123+
124+
- provider: str. Your OAuth Provider ID.
125+
126+
- access_token: typing.Optional[str]. Will be passed through your lookup URL to get the user ID. Required if no `code`.
127+
128+
- code: typing.Optional[str]. Will be passed to the OAuth token endpoint to get a token. Required if no `accessToken`.
129+
"""
130+
_request: typing.Dict[str, typing.Any] = {"provider": provider}
131+
if access_token is not OMIT:
132+
_request["accessToken"] = access_token
133+
if code is not OMIT:
134+
_request["code"] = code
135+
_response = await self._client_wrapper.httpx_client.request(
136+
"POST",
137+
urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", f"users/{handle}/o-auth-identities"),
138+
json=jsonable_encoder(_request),
139+
headers=self._client_wrapper.get_headers(),
140+
timeout=60,
141+
)
142+
if 200 <= _response.status_code < 300:
143+
return pydantic.parse_obj_as(UserResponse, _response.json()) # type: ignore
144+
try:
145+
_response_json = _response.json()
146+
except JSONDecodeError:
147+
raise ApiError(status_code=_response.status_code, body=_response.text)
148+
raise ApiError(status_code=_response.status_code, body=_response_json)
149+
150+
151+
def _get_base_url(*, base_url: typing.Optional[str] = None, environment: CodeCombatEnvironment) -> str:
152+
if base_url is not None:
153+
return base_url
154+
elif environment is not None:
155+
return environment.value
156+
else:
157+
raise Exception("Please pass in either base_url or environment to construct the client")

0 commit comments

Comments
 (0)