faceit 0.1.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.
faceit/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ from .__version__ import __version__
2
+ from ._faceit import AsyncFaceit, Faceit
3
+ from ._resources import (
4
+ AsyncPageIterator,
5
+ CollectReturnFormat,
6
+ SyncPageIterator,
7
+ check_pagination_support,
8
+ )
9
+ from .constants import (
10
+ ELO_THRESHOLDS,
11
+ EventCategory,
12
+ ExpandOption,
13
+ GameID,
14
+ HighTierLevel,
15
+ SkillLevel,
16
+ )
17
+ from .http import AsyncClient, SyncClient
18
+
19
+ __all__ = (
20
+ "ELO_THRESHOLDS",
21
+ "AsyncClient",
22
+ "AsyncFaceit",
23
+ "AsyncPageIterator",
24
+ "CollectReturnFormat",
25
+ "EventCategory",
26
+ "ExpandOption",
27
+ "Faceit",
28
+ "GameID",
29
+ "HighTierLevel",
30
+ "SkillLevel",
31
+ "SyncClient",
32
+ "SyncPageIterator",
33
+ "__version__",
34
+ "check_pagination_support",
35
+ )
faceit/__version__.py ADDED
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version(__package__ or __name__)
5
+ except PackageNotFoundError:
6
+ __version__ = "dev"
faceit/_faceit.py ADDED
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+ from abc import ABC
5
+ from warnings import warn
6
+
7
+ from ._repr import representation
8
+ from ._resources import AsyncResources, SyncResources
9
+ from ._typing import ClientT, ResourceT, Self, ValidUUID
10
+ from .constants import BASE_WIKI_URL
11
+ from .http import AsyncClient, SyncClient
12
+
13
+ if t.TYPE_CHECKING:
14
+ from types import TracebackType
15
+
16
+
17
+ @representation("client", "resources")
18
+ class BaseFaceit(t.Generic[ClientT, ResourceT], ABC):
19
+ __slots__ = "_client", "_resources"
20
+
21
+ _client_cls: t.Type[ClientT]
22
+ _resources_cls: t.Type[ResourceT]
23
+
24
+ @t.overload
25
+ def __init__(
26
+ self,
27
+ api_key: ValidUUID,
28
+ **client_options: t.Any,
29
+ ) -> None: ...
30
+
31
+ @t.overload
32
+ def __init__(
33
+ self,
34
+ *,
35
+ client: ClientT,
36
+ ) -> None: ...
37
+
38
+ def __init__(
39
+ self,
40
+ api_key: t.Optional[ValidUUID] = None,
41
+ *,
42
+ client: t.Optional[ClientT] = None,
43
+ **client_options: t.Any,
44
+ ) -> None:
45
+ if api_key is None and client is None:
46
+ raise ValueError("Either 'api_key' or 'client' must be provided")
47
+ if api_key is not None and client is not None:
48
+ raise ValueError("Provide either 'api_key' or 'client', not both")
49
+
50
+ if client is not None:
51
+ if client_options:
52
+ warn(
53
+ "'client_options' are ignored when an existing client "
54
+ "instance is provided. Configure your client before "
55
+ "passing it to this constructor.",
56
+ UserWarning,
57
+ stacklevel=2,
58
+ )
59
+ self._client = client
60
+ else:
61
+ self._client = self.__class__._client_cls(
62
+ api_key, **client_options
63
+ )
64
+
65
+ self._resources = self.__class__._resources_cls(self._client)
66
+
67
+ @property
68
+ def client(self) -> ClientT:
69
+ return self._client
70
+
71
+ @property
72
+ def resources(self) -> ResourceT:
73
+ return self._resources
74
+
75
+ def __str__(self) -> str:
76
+ return (
77
+ f"FACEIT API interface "
78
+ f"(resources and client, docs: {BASE_WIKI_URL})"
79
+ )
80
+
81
+
82
+ @t.final
83
+ class Faceit(BaseFaceit[SyncClient, SyncResources]):
84
+ __slots__ = ()
85
+
86
+ _client_cls = SyncClient
87
+ _resources_cls = SyncResources
88
+
89
+ def __enter__(self) -> Self:
90
+ self.client.__enter__()
91
+ return self
92
+
93
+ def __exit__(
94
+ self,
95
+ typ: t.Optional[t.Type[BaseException]],
96
+ exc: t.Optional[BaseException],
97
+ tb: t.Optional[TracebackType],
98
+ ) -> None:
99
+ self.client.__exit__(typ, exc, tb)
100
+
101
+
102
+ @t.final
103
+ class AsyncFaceit(BaseFaceit[AsyncClient, AsyncResources]):
104
+ __slots__ = ()
105
+
106
+ _client_cls = AsyncClient
107
+ _resources_cls = AsyncResources
108
+
109
+ MAX_CONCURRENT_REQUESTS: t.ClassVar[int] = (
110
+ AsyncClient.MAX_CONCURRENT_REQUESTS
111
+ )
112
+
113
+ async def __aenter__(self) -> Self:
114
+ await self.client.__aenter__()
115
+ return self
116
+
117
+ async def __aexit__(
118
+ self,
119
+ typ: t.Optional[t.Type[BaseException]],
120
+ exc: t.Optional[BaseException],
121
+ tb: t.Optional[TracebackType],
122
+ ) -> None:
123
+ await self.client.__aexit__(typ, exc, tb)
faceit/_repr.py ADDED
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ if t.TYPE_CHECKING:
6
+ from ._typing import TypeAlias
7
+
8
+ _ReprMethod: TypeAlias = t.Callable[[], str]
9
+ _ClassT = t.TypeVar("_ClassT", bound=t.Type)
10
+
11
+ _UNINITIALIZED_MARKER: t.Final = "uninitialized"
12
+
13
+
14
+ def _format_fields(
15
+ obj: t.Any, fields: t.Tuple[str, ...], joiner: str = " ", /
16
+ ) -> str:
17
+ return (
18
+ joiner.join(f"{field}={getattr(obj, field)!r}" for field in fields)
19
+ if all(hasattr(obj, field) for field in fields)
20
+ else repr(_UNINITIALIZED_MARKER)
21
+ )
22
+
23
+
24
+ def _apply_representation(
25
+ cls: _ClassT, fields: t.Tuple[str, ...], use_str: bool, /
26
+ ) -> _ClassT:
27
+ has_str = getattr(cls, "__str__", None) is not object.__str__
28
+
29
+ if use_str and not has_str:
30
+ raise TypeError(f"Class {cls.__name__} must define __str__ method")
31
+
32
+ def repr_(self: _ClassT) -> str:
33
+ str_args = (
34
+ f"'{self}'" if use_str else _format_fields(self, fields, ", ")
35
+ )
36
+ return f"{cls.__name__}({str_args})"
37
+
38
+ def str_(self: _ClassT) -> str:
39
+ return _format_fields(self, fields)
40
+
41
+ cls.__repr__ = t.cast(_ReprMethod, repr_)
42
+ if not has_str:
43
+ cls.__str__ = t.cast(_ReprMethod, str_)
44
+
45
+ return cls
46
+
47
+
48
+ def representation(
49
+ *fields: str, use_str: bool = False
50
+ ) -> t.Callable[[_ClassT], _ClassT]:
51
+ def decorator(cls: _ClassT) -> _ClassT:
52
+ return _apply_representation(cls, fields, use_str)
53
+
54
+ return decorator
@@ -0,0 +1,105 @@
1
+ import typing as t
2
+ from abc import ABC
3
+ from dataclasses import dataclass
4
+ from functools import cached_property
5
+
6
+ from faceit._typing import ClientT, Model, Raw
7
+ from faceit.http import AsyncClient, SyncClient
8
+
9
+ from ._base import BaseResource
10
+ from ._championships import (
11
+ AsyncChampionships,
12
+ BaseChampionships,
13
+ SyncChampionships,
14
+ )
15
+ from ._matches import AsyncMatches, BaseMatches, SyncMatches
16
+ from ._pagination import (
17
+ AsyncPageIterator,
18
+ BasePageIterator,
19
+ CollectReturnFormat,
20
+ SyncPageIterator,
21
+ TimestampPaginationConfig,
22
+ check_pagination_support,
23
+ )
24
+ from ._players import AsyncPlayers, BasePlayers, SyncPlayers
25
+
26
+ __all__ = (
27
+ "AsyncChampionships",
28
+ "AsyncMatches",
29
+ "AsyncPageIterator",
30
+ "AsyncPlayers",
31
+ "AsyncResources",
32
+ "BaseChampionships",
33
+ "BaseMatches",
34
+ "BasePageIterator",
35
+ "BasePlayers",
36
+ "BaseResource",
37
+ "BaseResources",
38
+ "CollectReturnFormat",
39
+ "SyncChampionships",
40
+ "SyncMatches",
41
+ "SyncPageIterator",
42
+ "SyncPlayers",
43
+ "SyncResources",
44
+ "TimestampPaginationConfig",
45
+ "check_pagination_support",
46
+ )
47
+
48
+
49
+ @dataclass(eq=False, frozen=True)
50
+ class BaseResources(t.Generic[ClientT], ABC):
51
+ _client: ClientT
52
+
53
+
54
+ @t.final
55
+ class SyncResources(BaseResources[SyncClient]):
56
+ @cached_property
57
+ def raw_championships(self) -> SyncChampionships[Raw]:
58
+ return SyncChampionships(self._client, raw=True)
59
+
60
+ @cached_property
61
+ def championships(self) -> SyncChampionships[Model]:
62
+ return SyncChampionships(self._client, raw=False)
63
+
64
+ @cached_property
65
+ def raw_matches(self) -> SyncMatches[Raw]:
66
+ return SyncMatches(self._client, raw=True)
67
+
68
+ @cached_property
69
+ def matches(self) -> SyncMatches[Model]:
70
+ return SyncMatches(self._client, raw=False)
71
+
72
+ @cached_property
73
+ def raw_players(self) -> SyncPlayers[Raw]:
74
+ return SyncPlayers(self._client, raw=True)
75
+
76
+ @cached_property
77
+ def players(self) -> SyncPlayers[Model]:
78
+ return SyncPlayers(self._client, raw=False)
79
+
80
+
81
+ @t.final
82
+ class AsyncResources(BaseResources[AsyncClient]):
83
+ @cached_property
84
+ def raw_championships(self) -> AsyncChampionships[Raw]:
85
+ return AsyncChampionships(self._client, raw=True)
86
+
87
+ @cached_property
88
+ def championships(self) -> AsyncChampionships[Model]:
89
+ return AsyncChampionships(self._client, raw=False)
90
+
91
+ @cached_property
92
+ def raw_matches(self) -> AsyncMatches[Raw]:
93
+ return AsyncMatches(self._client, raw=True)
94
+
95
+ @cached_property
96
+ def matches(self) -> AsyncMatches[Model]:
97
+ return AsyncMatches(self._client, raw=False)
98
+
99
+ @cached_property
100
+ def raw_players(self) -> AsyncPlayers[Raw]:
101
+ return AsyncPlayers(self._client, raw=True)
102
+
103
+ @cached_property
104
+ def players(self) -> AsyncPlayers[Model]:
105
+ return AsyncPlayers(self._client, raw=False)
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import typing as t
5
+ from abc import ABC
6
+ from dataclasses import dataclass
7
+ from warnings import warn
8
+
9
+ from pydantic import ValidationError
10
+ from strenum import StrEnum
11
+
12
+ from faceit._typing import (
13
+ ClientT,
14
+ ModelT,
15
+ NotRequired,
16
+ RawAPIPageResponse,
17
+ RawAPIResponse,
18
+ )
19
+ from faceit.http import Endpoint
20
+ from faceit.models import ItemPage
21
+
22
+ from ._pagination import (
23
+ AsyncPageIterator,
24
+ SyncPageIterator,
25
+ TimestampPaginationConfig,
26
+ )
27
+
28
+ if t.TYPE_CHECKING:
29
+ _ResponseT = t.TypeVar("_ResponseT", bound=RawAPIResponse)
30
+
31
+ _logger = logging.getLogger(__name__)
32
+
33
+ _KT = t.TypeVar("_KT")
34
+
35
+ # Temporary placeholder type for unimplemented models.
36
+ # Serves as a stub during development and should be replaced with
37
+ # concrete models as implementation progresses.
38
+ ModelPlaceholder: None = None
39
+
40
+
41
+ @t.final
42
+ class RequestPayload(t.TypedDict):
43
+ endpoint: Endpoint
44
+ params: t.Dict[str, t.Any]
45
+
46
+
47
+ @t.final
48
+ class MappedValidatorConfig(t.TypedDict, t.Generic[_KT, ModelT]):
49
+ validator_map: t.Dict[_KT, t.Type[ModelT]]
50
+ is_paged: bool
51
+ key_name: NotRequired[str]
52
+
53
+
54
+ @t.final
55
+ class FaceitResourcePath(StrEnum):
56
+ CHAMPIONSHIPS = "championships"
57
+ MATCHES = "matches"
58
+ PLAYERS = "players"
59
+
60
+
61
+ @dataclass(eq=False, frozen=True)
62
+ class BaseResource(t.Generic[ClientT], ABC):
63
+ __slots__ = "_client", "raw"
64
+
65
+ _client: ClientT
66
+ raw: bool
67
+
68
+ _sync_page_iterator: t.ClassVar = SyncPageIterator
69
+ _async_page_iterator: t.ClassVar = AsyncPageIterator
70
+ _timestamp_cfg: t.ClassVar = TimestampPaginationConfig
71
+
72
+ PATH: t.ClassVar[Endpoint]
73
+
74
+ _PARAM_NAME_MAP: t.ClassVar = {
75
+ "start": "from",
76
+ "category": "type",
77
+ }
78
+
79
+ def __init_subclass__(
80
+ cls,
81
+ *,
82
+ resource_path: t.Optional[FaceitResourcePath] = None,
83
+ **kwargs: t.Any,
84
+ ) -> None:
85
+ super().__init_subclass__(**kwargs)
86
+ if hasattr(cls, "PATH"):
87
+ return
88
+ if resource_path is None:
89
+ raise TypeError(
90
+ f"Class {cls.__name__} requires 'path' parameter or a "
91
+ f"parent with 'PATH' defined."
92
+ )
93
+ cls.PATH = Endpoint(resource_path)
94
+
95
+ # NOTE: These overloads are necessary as this function directly returns in resource
96
+ # methods, where typing must be strict for public API. Current implementation
97
+ # is sufficient, though alternative typing approaches could be considered.
98
+
99
+ # TODO: Replace named arguments with a single `config: MappedValidatorConfig`
100
+ # parameter, but this is currently not possible due to Python 3.8 compatibility
101
+ # issues with Generic type subscriptions. Once Python 3.8 support is dropped,
102
+ # this should be refactored.
103
+
104
+ @t.overload
105
+ def _process_response_with_mapped_validator(
106
+ self,
107
+ response: RawAPIPageResponse,
108
+ key: _KT,
109
+ /,
110
+ *,
111
+ validator_map: t.Dict[_KT, t.Type[ModelT]],
112
+ is_paged: t.Literal[False],
113
+ key_name: str = ...,
114
+ ) -> t.Union[ModelT, RawAPIPageResponse]: ...
115
+
116
+ @t.overload
117
+ def _process_response_with_mapped_validator(
118
+ self,
119
+ response: RawAPIPageResponse,
120
+ key: _KT,
121
+ /,
122
+ *,
123
+ validator_map: t.Dict[_KT, t.Type[ModelT]],
124
+ is_paged: t.Literal[True],
125
+ key_name: str = ...,
126
+ ) -> t.Union[ItemPage[ModelT], RawAPIPageResponse]: ...
127
+
128
+ def _process_response_with_mapped_validator(
129
+ self,
130
+ response: RawAPIPageResponse,
131
+ key: _KT,
132
+ /,
133
+ *,
134
+ validator_map: t.Dict[_KT, t.Type[ModelT]],
135
+ is_paged: bool,
136
+ key_name: str = "key",
137
+ ) -> t.Union[ModelT, ItemPage[ModelT], RawAPIPageResponse]:
138
+ _logger.debug(
139
+ "Processing response with mapped validator for key: %s", key
140
+ )
141
+
142
+ validator = validator_map.get(key)
143
+ if validator is None:
144
+ warn(
145
+ f"No model defined for {key_name} '{key}'. "
146
+ f"Consider using the raw response.",
147
+ UserWarning,
148
+ stacklevel=3,
149
+ )
150
+ return response
151
+
152
+ # Suppressing type checking warning because we're using a
153
+ # dynamic runtime subscript `ItemPage` is being subscripted
154
+ # with a variable (`validator`) which mypy cannot statically verify
155
+ return self._validate_response(
156
+ response,
157
+ t.cast(t.Type[ModelT], ItemPage[validator]) # type: ignore[valid-type]
158
+ if is_paged
159
+ else validator,
160
+ )
161
+
162
+ def _validate_response(
163
+ self,
164
+ response: _ResponseT,
165
+ validator: t.Optional[t.Type[ModelT]],
166
+ /,
167
+ ) -> t.Union[_ResponseT, ModelT]:
168
+ if validator is not None and not self.raw:
169
+ try:
170
+ return validator.model_validate(response)
171
+ except ValidationError:
172
+ _logger.exception(
173
+ "Response validation failed for %s model",
174
+ validator.__name__,
175
+ )
176
+ return response
177
+
178
+ @classmethod
179
+ def _build_params(cls, **params: t.Any) -> t.Dict[str, t.Any]:
180
+ return {
181
+ cls._PARAM_NAME_MAP.get(key, key): value
182
+ for key, value in params.items()
183
+ if value is not None
184
+ }