Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/ci.yaml → .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ name: CI

on:
push:
branches: ["main"]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.10", "3.12", "3.14"]
python-version: ["3.10.0", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ jobs:
run: uv build

- name: Publish to PyPI
run: uv publish dist/*
run: uv publish
3 changes: 0 additions & 3 deletions .mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@
exclude = (?x)(
^assets/
| ^docs/
| ^examples/
| ^tests/
)
local_partial_types = True
; We target Python 3.10+ for type checking because modern mypy releases
; have dropped support for Python 3.8, even though our library supports 3.8
python_version = 3.10
strict = True
warn_unreachable = True
Expand Down
1 change: 0 additions & 1 deletion .pytest.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
[pytest]
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function
markers =
e2e: End-to-end tests that interact with the real FACEIT API
19 changes: 9 additions & 10 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
line-length = 88
preview = true
target-version = "py38"
target-version = "py310"

[lint]
select = ["ALL"]
Expand All @@ -16,8 +16,8 @@ ignore = [
"PLR0904", # Too many public methods
"PLR0913", # Too many function arguments
"RUF003", # Ambiguous Unicode character in comment
"SLF001", # Private member accessed outside class
"S101", # Use of assert detected
"SLF001", # Private member accessed outside class
"TD", # flake8-todos violations
]

Expand All @@ -40,17 +40,20 @@ banned-from = [
"re",
"reprlib",
"tenacity",
"typing",
"warnings",
]

[lint.pep8-naming]
ignore-names = ["env", "pages"]
ignore-names = [
"env",
"pages",
]

[lint.per-file-ignores]
"__init__.py" = [
"RUF067", # Code is present in `__init__.py`
]
"docs/*" = ["ALL"]
"examples/*" = [
"ICN003", # Import a member from the module instead of importing the member directly
"INP001", # Missing `__init__.py` file in a package directory
Expand All @@ -62,15 +65,14 @@ ignore-names = ["env", "pages"]
"E302", # Expected 2 blank lines before top-level definitions
"E305", # Expected 2 blank lines after a class or function definition
]
"docs/*" = ["ALL"]
"scripts/*" = [
"T201", # `print()` statement detected
"INP001", # Missing `__init__.py` file in a package directory
"T201", # `print()` statement detected
]
"tests/*" = [
"PLR0917", # Too many positional arguments
"PLC1901", # Compare to empty string (e.g., `x == ""` instead of `not x`)
"PLC2701", # Import of a private name from an external module
"PLR0917", # Too many positional arguments
"PLR2004", # Magic value used in a comparison
"PLR6301", # Method could be a function, class method, or static method (doesn't use `self`)
"PT011", # `pytest.raises()` is too broad, set the `match` parameter
Expand All @@ -87,6 +89,3 @@ ignore-names = ["env", "pages"]
allow-dunder-method-names = [
"__get_pydantic_core_schema__",
]

[lint.pyupgrade]
keep-runtime-typing = true
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# FACEIT Python API Library

[![Python](https://img.shields.io/badge/Python-3.8%2B-FAD6C5?style=flat-square)](https://www.python.org/)
[![Python](https://img.shields.io/badge/Python-3.10%2B-FAD6C5?style=flat-square)](https://www.python.org/)
[![PyPI](https://img.shields.io/pypi/v/faceit?label=PyPI&style=flat-square&color=FAD6C5)](https://pypi.org/project/faceit/)
[![License](https://img.shields.io/badge/License-Apache_2.0-FAD6C5?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
[![Downloads](https://img.shields.io/pypi/dm/faceit?label=Downloads&style=flat-square&color=FAD6C5)](https://pypi.org/project/faceit/)
Expand All @@ -29,7 +29,7 @@

## Installation

> Requires **Python 3.8+**.
> Requires **Python 3.10+**.

```bash
pip install faceit
Expand Down
73 changes: 33 additions & 40 deletions examples/simple_discord_bot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from contextlib import suppress
from dataclasses import dataclass
from typing import Any, cast

Expand All @@ -8,6 +7,7 @@
from disnake.ext import commands

import faceit
import faceit.exceptions
from faceit.models.players import MatchResult


Expand All @@ -18,39 +18,39 @@ class StatsCommand(commands.Cog):

async def cog_slash_command_error( # noqa: PLR6301
self,
inter: disnake.ApplicationCommandInteraction[Any],
inter: disnake.CommandInteraction[Any],
error: Exception,
) -> None:
if isinstance(error, commands.CommandInvokeError):
error = error.original

player_name = inter.filled_options.get("player_name", "")

if isinstance(error, pydantic.ValidationError):
await inter.edit_original_response(
f"⚠️ We couldn't process the profile for **{player_name}**. "
"Please check if the nickname is entered correctly."
)
elif isinstance(error, faceit.exceptions.NotFoundError):
await inter.edit_original_response(
f"❌ Player **{player_name}** not found."
)
elif isinstance(error, faceit.APIError):
await inter.edit_original_response(
f"⚠️ API Error [{error.status_code}]: {error.message}"
)
else:
await inter.edit_original_response(
"💥 An unexpected error occurred. Please try again later."
)
player_name = inter.filled_options.get("player_name", "<unknown>")
match error:
case pydantic.ValidationError():
await inter.edit_original_response(
f"⚠️ We couldn't process the profile for `{player_name}`. "
"Please check if the nickname is entered correctly."
)
case faceit.exceptions.NotFoundError():
await inter.edit_original_response(
f"❌ Player `{player_name}` not found."
)
case faceit.exceptions.APIError():
await inter.edit_original_response(
f"⚠️ API Error (`{error.status_code}`): {error.message}"
)
case _:
await inter.edit_original_response(
"💥 An unexpected error occurred. Please try again later."
)

@commands.slash_command(
name="stats",
description="Show detailed FACEIT CS2 player statistics",
)
async def stats(
self,
inter: disnake.ApplicationCommandInteraction[Any],
inter: disnake.CommandInteraction[Any],
player_name: str = commands.Param(
description="FACEIT player nickname",
),
Expand All @@ -59,11 +59,10 @@ async def stats(

player = await self.faceit_data.players.get(player_name)

cs2_game = player.games.get(faceit.GameID.CS2)
if cs2_game is None:
cs2_stats = player.games.get(faceit.GameID.CS2)
if cs2_stats is None:
return await inter.edit_original_response(
f"🔎 Player **{player.nickname}** found, "
"but they don't have CS2 linked."
f"🔎 Player `{player.nickname}` found, but they don't have CS2 linked."
)

player_stats = await self.faceit_data.players.stats(
Expand All @@ -79,8 +78,8 @@ async def stats(
if player.avatar:
embed.set_thumbnail(url=player.avatar)

embed.add_field("🎮 Level", f"**{int(cs2_game.level)} LVL**", inline=True)
embed.add_field("📈 ELO", f"**{cs2_game.elo}**", inline=True)
embed.add_field("🎮 Level", f"**{int(cs2_stats.level)} LVL**", inline=True)
embed.add_field("📈 ELO", f"**{cs2_stats.elo}**", inline=True)
embed.add_field(
"📊 K/D",
f"**{player_stats.lifetime.average_kd_ratio}**",
Expand All @@ -98,14 +97,11 @@ async def stats(
)

if player_stats.lifetime.recent_results:
embed.add_field(
"Recent Results",
" ".join(
"✅" if result is MatchResult.WIN else "❌"
for result in player_stats.lifetime.recent_results
),
inline=False,
results = " ".join(
"✅" if result is MatchResult.WIN else "❌"
for result in player_stats.lifetime.recent_results
)
embed.add_field("Recent Results", results, inline=False)

embed.set_footer(text="Powered by faceit-python")
return await inter.edit_original_response(embed=embed)
Expand All @@ -119,17 +115,14 @@ async def main() -> None:
"DISCORD_BOT_TOKEN"
),
)
async with (
# NOTE: Ensure the `FACEIT_API_KEY` is set in your environment variables
# (Requires `faceit[env]` to be installed)
faceit.AsyncDataResource() # or use faceit.AsyncDataResource("YOUR_FACEIT_API_KEY")
) as data:
async with faceit.AsyncDataResource() as data:
bot.add_cog(StatsCommand(bot, data))
await bot.start(bot_token)


if __name__ == "__main__":
import asyncio
from contextlib import suppress

with suppress(KeyboardInterrupt, asyncio.CancelledError): # CTRL+C
asyncio.run(main())
39 changes: 17 additions & 22 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[project]
name = "faceit"
version = "0.2.3"
version = "0.3.0"
description = "The Python wrapper for the FACEIT API"
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.10"
license = "Apache-2.0"
authors = [
{ name = "zombyacoff", email = "zombyacoff@gmail.com" },
Expand All @@ -14,45 +14,40 @@ classifiers = [
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Typing :: Typed",
]
dependencies = [
"httpx>=0.27.0,<1.0.0",
"pydantic>=2.7.1,<3.0.0",
"tenacity>=8.2.3,<9.0.0",
"httpx>=0.28.0",
"pydantic>=2.13.0",
"tenacity>=8.5.0",
]

[project.optional-dependencies]
env = ["python-decouple>=3.8"]

[dependency-groups]
dev = [
"mypy>=1.14",
"pre-commit==3.5",
"pytest>=7.4",
"pytest-asyncio>=0.21.1",
"ruff>=0.4.8",
"mypy>=2.0.0",
"pre-commit>=3.5",
"pytest>=9.0.0",
"pytest-asyncio>=1.3.0",
"ruff>=0.15.0",
]
examples = [
"disnake>=2.0.0,<3.0.0",
# Maybe needed later
# "pydantic-settings>=2.0.0",
# "wireup>=2.0.0,<3.0.0",
"disnake>=2.0.0",
]

[project.urls]
"Documentation" = "https://docs.faceit.com/docs"

[tool.hatch.build.targets.wheel]
packages = ["src/faceit"]
"Bug Tracker" = "https://github.com/zombyacoff/faceit-python/issues"
"Releases" = "https://github.com/zombyacoff/faceit-python/releases"
"Repository" = "https://github.com/zombyacoff/faceit-python"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
requires = ["uv_build>=0.11.14"]
build-backend = "uv_build"
2 changes: 1 addition & 1 deletion scripts/clean.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import shutil
from pathlib import Path
from typing import Final # noqa: ICN003
from typing import Final

PYCACHE: Final = "__pycache__"
DIRS: Final = {
Expand Down
3 changes: 0 additions & 3 deletions src/faceit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,14 @@
pages,
)
from .constants import EventCategory, ExpandedField, GameID, Region, SkillLevel
from .faceit import AsyncFaceit, Faceit
from .http import FromEnv, MaxConcurrentRequests

__all__ = [
"AsyncDataResource",
"AsyncFaceit", # deprecated (remove in v0.3.0 ?)
"AsyncPageIterator",
"CollectReturnFormat",
"EventCategory",
"ExpandedField",
"Faceit", # deprecated (remove in v0.3.0 ?)
"FromEnv",
"GameID",
"MaxConcurrentRequests",
Expand Down
Loading
Loading