magic_hour 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of magic_hour might be problematic. Click here for more details.

Files changed (131) hide show
  1. magic_hour/__init__.py +6 -0
  2. magic_hour/client.py +61 -0
  3. magic_hour/core/__init__.py +53 -0
  4. magic_hour/core/api_error.py +48 -0
  5. magic_hour/core/auth.py +314 -0
  6. magic_hour/core/base_client.py +600 -0
  7. magic_hour/core/binary_response.py +23 -0
  8. magic_hour/core/request.py +158 -0
  9. magic_hour/core/response.py +293 -0
  10. magic_hour/core/type_utils.py +28 -0
  11. magic_hour/core/utils.py +38 -0
  12. magic_hour/environment.py +6 -0
  13. magic_hour/resources/v1/__init__.py +4 -0
  14. magic_hour/resources/v1/ai_clothes_changer/README.md +41 -0
  15. magic_hour/resources/v1/ai_clothes_changer/__init__.py +4 -0
  16. magic_hour/resources/v1/ai_clothes_changer/client.py +129 -0
  17. magic_hour/resources/v1/ai_headshot_generator/README.md +31 -0
  18. magic_hour/resources/v1/ai_headshot_generator/__init__.py +4 -0
  19. magic_hour/resources/v1/ai_headshot_generator/client.py +119 -0
  20. magic_hour/resources/v1/ai_image_generator/README.md +37 -0
  21. magic_hour/resources/v1/ai_image_generator/__init__.py +4 -0
  22. magic_hour/resources/v1/ai_image_generator/client.py +144 -0
  23. magic_hour/resources/v1/ai_image_upscaler/README.md +37 -0
  24. magic_hour/resources/v1/ai_image_upscaler/__init__.py +4 -0
  25. magic_hour/resources/v1/ai_image_upscaler/client.py +143 -0
  26. magic_hour/resources/v1/ai_photo_editor/README.md +53 -0
  27. magic_hour/resources/v1/ai_photo_editor/__init__.py +4 -0
  28. magic_hour/resources/v1/ai_photo_editor/client.py +167 -0
  29. magic_hour/resources/v1/ai_qr_code_generator/README.md +35 -0
  30. magic_hour/resources/v1/ai_qr_code_generator/__init__.py +4 -0
  31. magic_hour/resources/v1/ai_qr_code_generator/client.py +127 -0
  32. magic_hour/resources/v1/animation/README.md +63 -0
  33. magic_hour/resources/v1/animation/__init__.py +4 -0
  34. magic_hour/resources/v1/animation/client.py +179 -0
  35. magic_hour/resources/v1/client.py +153 -0
  36. magic_hour/resources/v1/face_swap/README.md +52 -0
  37. magic_hour/resources/v1/face_swap/__init__.py +4 -0
  38. magic_hour/resources/v1/face_swap/client.py +165 -0
  39. magic_hour/resources/v1/face_swap_photo/README.md +39 -0
  40. magic_hour/resources/v1/face_swap_photo/__init__.py +4 -0
  41. magic_hour/resources/v1/face_swap_photo/client.py +127 -0
  42. magic_hour/resources/v1/files/__init__.py +4 -0
  43. magic_hour/resources/v1/files/client.py +19 -0
  44. magic_hour/resources/v1/files/upload_urls/README.md +56 -0
  45. magic_hour/resources/v1/files/upload_urls/__init__.py +4 -0
  46. magic_hour/resources/v1/files/upload_urls/client.py +142 -0
  47. magic_hour/resources/v1/image_background_remover/README.md +31 -0
  48. magic_hour/resources/v1/image_background_remover/__init__.py +4 -0
  49. magic_hour/resources/v1/image_background_remover/client.py +121 -0
  50. magic_hour/resources/v1/image_projects/README.md +63 -0
  51. magic_hour/resources/v1/image_projects/__init__.py +4 -0
  52. magic_hour/resources/v1/image_projects/client.py +177 -0
  53. magic_hour/resources/v1/image_to_video/README.md +44 -0
  54. magic_hour/resources/v1/image_to_video/__init__.py +4 -0
  55. magic_hour/resources/v1/image_to_video/client.py +165 -0
  56. magic_hour/resources/v1/lip_sync/README.md +54 -0
  57. magic_hour/resources/v1/lip_sync/__init__.py +4 -0
  58. magic_hour/resources/v1/lip_sync/client.py +177 -0
  59. magic_hour/resources/v1/text_to_video/README.md +40 -0
  60. magic_hour/resources/v1/text_to_video/__init__.py +4 -0
  61. magic_hour/resources/v1/text_to_video/client.py +150 -0
  62. magic_hour/resources/v1/video_projects/README.md +63 -0
  63. magic_hour/resources/v1/video_projects/__init__.py +4 -0
  64. magic_hour/resources/v1/video_projects/client.py +177 -0
  65. magic_hour/resources/v1/video_to_video/README.md +60 -0
  66. magic_hour/resources/v1/video_to_video/__init__.py +4 -0
  67. magic_hour/resources/v1/video_to_video/client.py +204 -0
  68. magic_hour/types/models/__init__.py +60 -0
  69. magic_hour/types/models/get_v1_image_projects_id_response.py +82 -0
  70. magic_hour/types/models/get_v1_image_projects_id_response_downloads_item.py +19 -0
  71. magic_hour/types/models/get_v1_image_projects_id_response_error.py +25 -0
  72. magic_hour/types/models/get_v1_video_projects_id_response.py +114 -0
  73. magic_hour/types/models/get_v1_video_projects_id_response_download.py +19 -0
  74. magic_hour/types/models/get_v1_video_projects_id_response_downloads_item.py +19 -0
  75. magic_hour/types/models/get_v1_video_projects_id_response_error.py +25 -0
  76. magic_hour/types/models/post_v1_ai_clothes_changer_response.py +25 -0
  77. magic_hour/types/models/post_v1_ai_headshot_generator_response.py +25 -0
  78. magic_hour/types/models/post_v1_ai_image_generator_response.py +25 -0
  79. magic_hour/types/models/post_v1_ai_image_upscaler_response.py +25 -0
  80. magic_hour/types/models/post_v1_ai_photo_editor_response.py +25 -0
  81. magic_hour/types/models/post_v1_ai_qr_code_generator_response.py +25 -0
  82. magic_hour/types/models/post_v1_animation_response.py +25 -0
  83. magic_hour/types/models/post_v1_face_swap_photo_response.py +25 -0
  84. magic_hour/types/models/post_v1_face_swap_response.py +25 -0
  85. magic_hour/types/models/post_v1_files_upload_urls_response.py +21 -0
  86. magic_hour/types/models/post_v1_files_upload_urls_response_items_item.py +31 -0
  87. magic_hour/types/models/post_v1_image_background_remover_response.py +25 -0
  88. magic_hour/types/models/post_v1_image_to_video_response.py +25 -0
  89. magic_hour/types/models/post_v1_lip_sync_response.py +25 -0
  90. magic_hour/types/models/post_v1_text_to_video_response.py +25 -0
  91. magic_hour/types/models/post_v1_video_to_video_response.py +25 -0
  92. magic_hour/types/params/__init__.py +205 -0
  93. magic_hour/types/params/post_v1_ai_clothes_changer_body.py +40 -0
  94. magic_hour/types/params/post_v1_ai_clothes_changer_body_assets.py +45 -0
  95. magic_hour/types/params/post_v1_ai_headshot_generator_body.py +40 -0
  96. magic_hour/types/params/post_v1_ai_headshot_generator_body_assets.py +28 -0
  97. magic_hour/types/params/post_v1_ai_image_generator_body.py +54 -0
  98. magic_hour/types/params/post_v1_ai_image_generator_body_style.py +28 -0
  99. magic_hour/types/params/post_v1_ai_image_upscaler_body.py +54 -0
  100. magic_hour/types/params/post_v1_ai_image_upscaler_body_assets.py +28 -0
  101. magic_hour/types/params/post_v1_ai_image_upscaler_body_style.py +36 -0
  102. magic_hour/types/params/post_v1_ai_photo_editor_body.py +63 -0
  103. magic_hour/types/params/post_v1_ai_photo_editor_body_assets.py +28 -0
  104. magic_hour/types/params/post_v1_ai_photo_editor_body_style.py +67 -0
  105. magic_hour/types/params/post_v1_ai_qr_code_generator_body.py +45 -0
  106. magic_hour/types/params/post_v1_ai_qr_code_generator_body_style.py +28 -0
  107. magic_hour/types/params/post_v1_animation_body.py +84 -0
  108. magic_hour/types/params/post_v1_animation_body_assets.py +55 -0
  109. magic_hour/types/params/post_v1_animation_body_style.py +279 -0
  110. magic_hour/types/params/post_v1_face_swap_body.py +72 -0
  111. magic_hour/types/params/post_v1_face_swap_body_assets.py +52 -0
  112. magic_hour/types/params/post_v1_face_swap_photo_body.py +40 -0
  113. magic_hour/types/params/post_v1_face_swap_photo_body_assets.py +36 -0
  114. magic_hour/types/params/post_v1_files_upload_urls_body.py +31 -0
  115. magic_hour/types/params/post_v1_files_upload_urls_body_items_item.py +38 -0
  116. magic_hour/types/params/post_v1_image_background_remover_body.py +40 -0
  117. magic_hour/types/params/post_v1_image_background_remover_body_assets.py +28 -0
  118. magic_hour/types/params/post_v1_image_to_video_body.py +73 -0
  119. magic_hour/types/params/post_v1_image_to_video_body_assets.py +28 -0
  120. magic_hour/types/params/post_v1_image_to_video_body_style.py +29 -0
  121. magic_hour/types/params/post_v1_lip_sync_body.py +80 -0
  122. magic_hour/types/params/post_v1_lip_sync_body_assets.py +52 -0
  123. magic_hour/types/params/post_v1_text_to_video_body.py +57 -0
  124. magic_hour/types/params/post_v1_text_to_video_body_style.py +28 -0
  125. magic_hour/types/params/post_v1_video_to_video_body.py +93 -0
  126. magic_hour/types/params/post_v1_video_to_video_body_assets.py +44 -0
  127. magic_hour/types/params/post_v1_video_to_video_body_style.py +199 -0
  128. magic_hour-0.8.0.dist-info/LICENSE +21 -0
  129. magic_hour-0.8.0.dist-info/METADATA +138 -0
  130. magic_hour-0.8.0.dist-info/RECORD +131 -0
  131. magic_hour-0.8.0.dist-info/WHEEL +4 -0
magic_hour/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .core import ApiError, BinaryResponse
2
+ from .client import AsyncClient, Client
3
+ from .environment import Environment
4
+
5
+
6
+ __all__ = ["ApiError", "AsyncClient", "BinaryResponse", "Client", "Environment"]
magic_hour/client.py ADDED
@@ -0,0 +1,61 @@
1
+ import httpx
2
+ import typing
3
+
4
+ from magic_hour.core import AsyncBaseClient, AuthBearer, SyncBaseClient
5
+ from magic_hour.environment import Environment
6
+ from magic_hour.resources.v1 import AsyncV1Client, V1Client
7
+
8
+
9
+ class Client:
10
+ def __init__(
11
+ self,
12
+ *,
13
+ base_url: typing.Optional[str] = None,
14
+ timeout: typing.Optional[float] = 60,
15
+ httpx_client: typing.Optional[httpx.Client] = None,
16
+ environment: Environment = Environment.ENVIRONMENT,
17
+ token: typing.Optional[str] = None,
18
+ ):
19
+ self._base_client = SyncBaseClient(
20
+ base_url=_get_base_url(base_url=base_url, environment=environment),
21
+ httpx_client=(
22
+ httpx.Client(timeout=timeout) if httpx_client is None else httpx_client
23
+ ),
24
+ )
25
+
26
+ self.v1 = V1Client(base_client=self._base_client)
27
+ self._base_client.register_auth("bearerAuth", AuthBearer(val=token))
28
+
29
+
30
+ class AsyncClient:
31
+ def __init__(
32
+ self,
33
+ *,
34
+ base_url: typing.Optional[str] = None,
35
+ timeout: typing.Optional[float] = 60,
36
+ httpx_client: typing.Optional[httpx.AsyncClient] = None,
37
+ environment: Environment = Environment.ENVIRONMENT,
38
+ token: typing.Optional[str] = None,
39
+ ):
40
+ self._base_client = AsyncBaseClient(
41
+ base_url=_get_base_url(base_url=base_url, environment=environment),
42
+ httpx_client=(
43
+ httpx.AsyncClient(timeout=timeout)
44
+ if httpx_client is None
45
+ else httpx_client
46
+ ),
47
+ )
48
+
49
+ self.v1 = AsyncV1Client(base_client=self._base_client)
50
+ self._base_client.register_auth("bearerAuth", AuthBearer(val=token))
51
+
52
+
53
+ def _get_base_url(
54
+ *, base_url: typing.Optional[str] = None, environment: Environment
55
+ ) -> str:
56
+ if base_url is not None:
57
+ return base_url
58
+ elif environment is not None:
59
+ return environment.value
60
+ else:
61
+ raise Exception("Must include a base_url or environment arguments")
@@ -0,0 +1,53 @@
1
+ from .api_error import ApiError
2
+ from .auth import (
3
+ AuthKeyQuery,
4
+ AuthBasic,
5
+ AuthBearer,
6
+ AuthProvider,
7
+ AuthKeyCookie,
8
+ AuthKeyHeader,
9
+ GrantType,
10
+ OAuth2,
11
+ OAuth2ClientCredentialsForm,
12
+ OAuth2PasswordForm,
13
+ )
14
+ from .base_client import AsyncBaseClient, BaseClient, SyncBaseClient
15
+ from .binary_response import BinaryResponse
16
+ from .request import (
17
+ encode_param,
18
+ filter_not_given,
19
+ to_content,
20
+ to_encodable,
21
+ RequestOptions,
22
+ default_request_options,
23
+ QueryParams,
24
+ )
25
+ from .response import from_encodable, AsyncStreamResponse, StreamResponse
26
+
27
+ __all__ = [
28
+ "ApiError",
29
+ "AsyncBaseClient",
30
+ "BaseClient",
31
+ "BinaryResponse",
32
+ "RequestOptions",
33
+ "default_request_options",
34
+ "SyncBaseClient",
35
+ "AuthKeyQuery",
36
+ "AuthBasic",
37
+ "AuthBearer",
38
+ "AuthProvider",
39
+ "AuthKeyCookie",
40
+ "AuthKeyHeader",
41
+ "GrantType",
42
+ "OAuth2",
43
+ "OAuth2ClientCredentialsForm",
44
+ "OAuth2PasswordForm",
45
+ "to_encodable",
46
+ "filter_not_given",
47
+ "to_content",
48
+ "encode_param",
49
+ "from_encodable",
50
+ "AsyncStreamResponse",
51
+ "StreamResponse",
52
+ "QueryParams",
53
+ ]
@@ -0,0 +1,48 @@
1
+ import typing
2
+
3
+
4
+ class ApiError(Exception):
5
+ """
6
+ A custom exception class for handling API-related errors.
7
+
8
+ This class extends the base Exception class to provide additional context
9
+ for API errors, including the HTTP status code and response body.
10
+
11
+ Attributes:
12
+ status_code: The HTTP status code associated with the error.
13
+ None if no status code is applicable.
14
+ body: The response body or error message content.
15
+ Can be any type depending on the API response format.
16
+ """
17
+
18
+ status_code: typing.Optional[int]
19
+ body: typing.Any
20
+
21
+ def __init__(
22
+ self, *, status_code: typing.Optional[int] = None, body: typing.Any = None
23
+ ) -> None:
24
+ """
25
+ Initialize the ApiError with optional status code and body.
26
+
27
+ Args:
28
+ status_code: The HTTP status code of the error.
29
+ Defaults to None.
30
+ body: The response body or error message content.
31
+ Defaults to None.
32
+
33
+ Note:
34
+ The asterisk (*) in the parameters forces keyword arguments,
35
+ making the instantiation more explicit.
36
+ """
37
+ self.status_code = status_code
38
+ self.body = body
39
+
40
+ def __str__(self) -> str:
41
+ """
42
+ Return a string representation of the ApiError.
43
+
44
+ Returns:
45
+ str: A formatted string containing the status code and body.
46
+ Format: "status_code: {status_code}, body: {body}"
47
+ """
48
+ return f"status_code: {self.status_code}, body: {self.body}"
@@ -0,0 +1,314 @@
1
+ import abc
2
+ import datetime
3
+ from typing import Any, Dict, TypedDict, Optional, List, Tuple, Literal
4
+
5
+ import jsonpointer # type: ignore
6
+ import httpx
7
+ from pydantic import BaseModel
8
+ from .request import RequestConfig
9
+
10
+
11
+ class AuthProvider(abc.ABC, BaseModel):
12
+ """
13
+ Abstract base class defining the interface for authentication providers.
14
+
15
+ Each concrete implementation handles a specific authentication method
16
+ and modifies the request configuration accordingly.
17
+ """
18
+
19
+ @abc.abstractmethod
20
+ def add_to_request(self, cfg: RequestConfig) -> RequestConfig:
21
+ """
22
+ Adds authentication details to the request configuration.
23
+
24
+ Args:
25
+ cfg: The request configuration to modify
26
+
27
+ Returns:
28
+ The modified request configuration with authentication details added
29
+ """
30
+
31
+ @abc.abstractmethod
32
+ def set_value(self, val: Optional[str]) -> None:
33
+ """
34
+ Generic method to set an auth value.
35
+
36
+ Args:
37
+ val: Authentication value to set
38
+ """
39
+
40
+
41
+ class AuthBasic(AuthProvider):
42
+ """
43
+ Implements HTTP Basic Authentication.
44
+
45
+ Adds username and password credentials to the request using the standard
46
+ HTTP Basic Authentication scheme.
47
+ """
48
+
49
+ username: Optional[str]
50
+ password: Optional[str]
51
+
52
+ def add_to_request(self, cfg: RequestConfig) -> RequestConfig:
53
+ """
54
+ Adds Basic Authentication credentials to the request configuration.
55
+
56
+ Only modifies the configuration if both username and password are provided.
57
+ """
58
+ if self.username is not None and self.password is not None:
59
+ cfg["auth"] = (self.username, self.password)
60
+ return cfg
61
+
62
+ def set_value(self, val: Optional[str]) -> None:
63
+ """
64
+ Sets value as the username
65
+ """
66
+ self.username = val
67
+
68
+
69
+ class AuthBearer(AuthProvider):
70
+ """
71
+ Implements Bearer token authentication.
72
+
73
+ Adds a Bearer token to the request's Authorization header following
74
+ the OAuth 2.0 Bearer Token scheme.
75
+ """
76
+
77
+ val: Optional[str]
78
+
79
+ def add_to_request(self, cfg: RequestConfig) -> RequestConfig:
80
+ """
81
+ Adds Bearer token to the Authorization header.
82
+
83
+ Only modifies the configuration if a token value is provided.
84
+ """
85
+ if self.val is not None:
86
+ headers = cfg.get("headers", dict())
87
+ headers["Authorization"] = f"Bearer {self.val}"
88
+ cfg["headers"] = headers
89
+ return cfg
90
+
91
+ def set_value(self, val: Optional[str]) -> None:
92
+ """
93
+ Sets value as the bearer token
94
+ """
95
+ self.val = val
96
+
97
+
98
+ class AuthKeyQuery(AuthProvider):
99
+ """
100
+ Implements query parameter-based authentication.
101
+
102
+ Adds an authentication token or key as a query parameter with a
103
+ configurable parameter name.
104
+ """
105
+
106
+ query_name: str
107
+ val: Optional[str]
108
+
109
+ def add_to_request(self, cfg: RequestConfig) -> RequestConfig:
110
+ """
111
+ Adds authentication value as a query parameter.
112
+
113
+ Only modifies the configuration if a value is provided.
114
+ """
115
+ if self.val is not None:
116
+ params = cfg.get("params", dict())
117
+ params[self.query_name] = self.val
118
+ cfg["params"] = params
119
+ return cfg
120
+
121
+ def set_value(self, val: Optional[str]) -> None:
122
+ """
123
+ Sets value as the key
124
+ """
125
+ self.val = val
126
+
127
+
128
+ class AuthKeyHeader(AuthProvider):
129
+ """
130
+ Implements header-based authentication.
131
+
132
+ Adds an authentication token or key as a custom header with a
133
+ configurable header name.
134
+ """
135
+
136
+ header_name: str
137
+ val: Optional[str]
138
+
139
+ def add_to_request(self, cfg: RequestConfig) -> RequestConfig:
140
+ """
141
+ Adds authentication value as a custom header.
142
+
143
+ Only modifies the configuration if a value is provided.
144
+ """
145
+ if self.val is not None:
146
+ headers = cfg.get("headers", {})
147
+ headers[self.header_name] = self.val
148
+ cfg["headers"] = headers
149
+ return cfg
150
+
151
+ def set_value(self, val: Optional[str]) -> None:
152
+ """
153
+ Sets value as the key
154
+ """
155
+ self.val = val
156
+
157
+
158
+ class AuthKeyCookie(AuthProvider):
159
+ """
160
+ Implements cookie-based authentication.
161
+
162
+ Adds an authentication token or key as a cookie with a
163
+ configurable cookie name.
164
+ """
165
+
166
+ cookie_name: str
167
+ val: Optional[str]
168
+
169
+ def add_to_request(self, cfg: RequestConfig) -> RequestConfig:
170
+ """
171
+ Adds authentication value as a cookie.
172
+
173
+ Only modifies the configuration if a value is provided.
174
+ """
175
+ if self.val is not None:
176
+ cookies = cfg.get("cookies", dict())
177
+ cookies[self.cookie_name] = self.val
178
+ cfg["cookies"] = cookies
179
+ return cfg
180
+
181
+ def set_value(self, val: Optional[str]) -> None:
182
+ """
183
+ Sets value as the key
184
+ """
185
+ self.val = val
186
+
187
+
188
+ class OAuth2PasswordForm(TypedDict, total=True):
189
+ """
190
+ OAuth2 authentication form for a password flow
191
+
192
+ Details:
193
+ https://datatracker.ietf.org/doc/html/rfc6749#section-4.3
194
+ """
195
+
196
+ username: str
197
+ password: str
198
+ client_id: Optional[str]
199
+ client_secret: Optional[str]
200
+ grant_type: Optional[str]
201
+ scope: Optional[List[str]]
202
+
203
+
204
+ class OAuth2ClientCredentialsForm(TypedDict, total=True):
205
+ """
206
+ OAuth2 authentication form for a client credentials flow
207
+
208
+ Details:
209
+ https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
210
+ """
211
+
212
+ client_id: str
213
+ client_secret: str
214
+ grant_type: Optional[str]
215
+ scope: Optional[List[str]]
216
+
217
+
218
+ GrantType = Literal["password", "client_credentials"]
219
+ CredentialsLocation = Literal["request_body", "basic_authorization_header"]
220
+ BodyContent = Literal["form", "json"]
221
+
222
+
223
+ class OAuth2(AuthProvider):
224
+ """
225
+ Implements OAuth2 token retrieval and refreshing.
226
+ Currently supports `password` and `client_credentials`
227
+ grant types.
228
+ """
229
+
230
+ # OAuth2 provider configuration
231
+ token_url: str
232
+ access_token_pointer: str
233
+ expires_in_pointer: str
234
+ credentials_location: CredentialsLocation
235
+ body_content: BodyContent
236
+ request_mutator: AuthProvider
237
+
238
+ # OAuth2 access token request values
239
+ grant_type: GrantType
240
+ username: Optional[str] = None
241
+ password: Optional[str] = None
242
+ client_id: Optional[str] = None
243
+ client_secret: Optional[str] = None
244
+ scope: Optional[List[str]] = None
245
+
246
+ # access_token storage
247
+ access_token: Optional[str] = None
248
+ expires_at: Optional[datetime.datetime] = None
249
+
250
+ def _refresh(self) -> Tuple[str, datetime.datetime]:
251
+ req_cfg: Dict[str, Any] = {"url": self.token_url}
252
+ req_data: Dict[str, Any] = {"grant_type": self.grant_type}
253
+
254
+ # add client credentials
255
+ if self.client_id is not None or self.client_secret is not None:
256
+ if self.credentials_location == "basic_authorization_header":
257
+ req_cfg["auth"] = (self.client_id, self.client_secret)
258
+ else:
259
+ req_data["client_id"] = self.client_id
260
+ req_data["client_secret"] = self.client_secret
261
+
262
+ # construct request data
263
+ if self.username is not None:
264
+ req_data["username"] = self.username
265
+ if self.password is not None:
266
+ req_data["password"] = self.password
267
+ if self.scope is not None:
268
+ req_data["scope"] = " ".join(self.scope)
269
+
270
+ if self.body_content == "json":
271
+ req_cfg["json"] = req_data
272
+ req_cfg["headers"] = {"content-type": "application/json"}
273
+ else:
274
+ req_cfg["data"] = req_data
275
+ req_cfg["headers"] = {"content-type": "application/x-www-form-urlencoded"}
276
+
277
+ # make access token request
278
+ token_res = httpx.post(**req_cfg)
279
+ token_res.raise_for_status()
280
+
281
+ # retrieve access token & optional expiry seconds
282
+ token_res_json: Dict[str, Any] = token_res.json()
283
+ access_token = str(
284
+ jsonpointer.resolve_pointer(token_res_json, self.access_token_pointer)
285
+ )
286
+
287
+ expires_in_secs = jsonpointer.resolve_pointer(
288
+ token_res_json, self.expires_in_pointer
289
+ )
290
+ if not isinstance(expires_in_secs, int):
291
+ expires_in_secs = 600
292
+ expires_at = datetime.datetime.now() + datetime.timedelta(
293
+ seconds=(
294
+ expires_in_secs - 60
295
+ ) # subtract a minute from the expiry as a buffer
296
+ )
297
+
298
+ return (access_token, expires_at)
299
+
300
+ def add_to_request(self, cfg: RequestConfig) -> RequestConfig:
301
+ token_expired = (
302
+ self.expires_at is not None and self.expires_at <= datetime.datetime.now()
303
+ )
304
+
305
+ if self.access_token is None or token_expired:
306
+ access_token, expires_at = self._refresh()
307
+ self.expires_at = expires_at
308
+ self.access_token = access_token
309
+
310
+ self.request_mutator.set_value(self.access_token)
311
+ return self.request_mutator.add_to_request(cfg)
312
+
313
+ def set_value(self, _val: Optional[str]) -> None:
314
+ raise NotImplementedError("an OAuth2 auth provider cannot be a request_mutator")