Skip to content
Open
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
8 changes: 8 additions & 0 deletions docs/contributor-guide/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ merge" button in GitHub 🥳.
Your PR is now merged into kloppy! We'll shout out your contribution in the
release notes.

### Optional dependencies

Optional dependencies (e.g. `polars`) should be imported with the helper `kloppy._utils.optional.import_optional_dependency`. This ensures a consistent error message when the dependency is not met.

All methods using an optional dependency should include a test asserting that an `ImportError` is raised when the optional dependency is not found. This test should be skipped if the library is present.

All optional dependencies should be documented in the [Optional dependencies](../user-guide/installation/index.md#optional-dependencies) section of the user guide and the minimum required version should be set in the `kloppy._utils.VERSIONS` dict.

## Documentation contributions

Contributing to the documentation benefits everyone who uses kloppy. If
Expand Down
29 changes: 28 additions & 1 deletion docs/user-guide/installation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Before you can use kloppy, you'll need to get it installed. This guide will guid

## Install Python

Being a Python library, kloppy requires Python. Currently, kloppy supports Python version 3.9 — 3.12. Get the latest version of Python at [python.org](https://www.python.org/downloads/) or with your operating system's package manager.
Being a Python library, kloppy requires Python. Currently, kloppy supports Python version 3.9 — 3.13. Get the latest version of Python at [python.org](https://www.python.org/downloads/) or with your operating system's package manager.

You can verify that Python is installed by typing `python` from your shell; you should see something like:

Expand All @@ -29,6 +29,7 @@ $ python -m pip install kloppy

You might have to install pip first. The easiest method is to use the [standalone pip installer](https://pip.pypa.io/en/latest/installing/#installing-with-get-pip-py).


### Installing the development version

Kloppy is actively developed on GitHub, where the code is [always available](https://github.com/PySport/kloppy). You can easily install the development version with:
Expand Down Expand Up @@ -64,3 +65,29 @@ To verify that kloppy can be seen by Python, type `python` from your shell. Then
>>> import kloppy
>>> print(kloppy.__version__)
```

## Optional dependencies

Kloppy has a few optional dependencies that are only required for specific functionality. For example, loading datasets directly from an S3 bucket requires `s3fs`. If an optional dependency is missing, kloppy raises an `ImportError` when the corresponding functionality is used.

The available optional dependencies are:

- `polars` (extra: `polars`): required to export datasets to Polars DataFrames with [`to_df()`][kloppy.domain.Dataset.to_df].
- `pandas` (extra: `pandas`): required to export datasets to pandas DataFrames with [`to_df()`][kloppy.domain.Dataset.to_df].
- `pyarrow` (extra: `pyarrow`): required to export datasets to pandas DataFrames backed by PyArrow with [`to_df()`][kloppy.domain.Dataset.to_df].
- `networkx` (extra: `query`): used by the `event_pattern_matching` module for sequence and pattern queries.
- `s3fs` (extra: `s3`): required to load datasets directly from Amazon S3 buckets.

With pip, you can install a subset of these dependencies with:

```console
$ python -m pip install "kloppy[pandas,s3]"
```

Or, alternatively, to install all optional dependencies:

```console
$ python -m pip install "kloppy[all]"
```


2 changes: 1 addition & 1 deletion kloppy/_providers/metrica.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Optional, Union

from kloppy._utils import github_resolve_raw_data_url
from kloppy.config import get_config
from kloppy.domain import EventDataset, EventFactory, TrackingDataset
from kloppy.exceptions import KloppyError
Expand All @@ -16,7 +17,6 @@
MetricaEPTSTrackingDataInputs,
)
from kloppy.io import FileLike, open_as_file
from kloppy.utils import github_resolve_raw_data_url


def load_tracking_csv(
Expand Down
2 changes: 1 addition & 1 deletion kloppy/_providers/skillcorner.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import json
from typing import Optional, Union

from kloppy._utils import github_resolve_raw_data_url
from kloppy.domain import TrackingDataset
from kloppy.exceptions import DeserializationError
from kloppy.infra.serializers.tracking.skillcorner import (
SkillCornerDeserializer,
SkillCornerInputs,
)
from kloppy.io import FileLike, open_as_file
from kloppy.utils import github_resolve_raw_data_url


def load(
Expand Down
2 changes: 1 addition & 1 deletion kloppy/_providers/sportec.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Optional

from kloppy._utils import deprecated
from kloppy.config import get_config
from kloppy.domain import EventDataset, EventFactory, TrackingDataset
from kloppy.infra.serializers.event.sportec import (
Expand All @@ -11,7 +12,6 @@
SportecTrackingDataInputs,
)
from kloppy.io import FileLike, open_as_file
from kloppy.utils import deprecated


def load_event(
Expand Down
2 changes: 1 addition & 1 deletion kloppy/_providers/statsbomb.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional, Union
import warnings

from kloppy._utils import github_resolve_raw_data_url
from kloppy.config import get_config
from kloppy.domain import EventDataset, EventFactory
from kloppy.domain.models.statsbomb.event import StatsBombEventFactory
Expand All @@ -9,7 +10,6 @@
StatsBombInputs,
)
from kloppy.io import FileLike, Source, open_as_file
from kloppy.utils import github_resolve_raw_data_url


def load(
Expand Down
2 changes: 1 addition & 1 deletion kloppy/_providers/statsperform.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Optional

from kloppy._utils import deprecated
from kloppy.config import get_config
from kloppy.domain import EventDataset, EventFactory, Provider, TrackingDataset
from kloppy.infra.serializers.event.statsperform import (
Expand All @@ -15,7 +16,6 @@
StatsPerformInputs as StatsPerformTrackingInputs,
)
from kloppy.io import FileLike, open_as_file
from kloppy.utils import deprecated


@deprecated("statsperform.load_tracking should be used")
Expand Down
2 changes: 1 addition & 1 deletion kloppy/_providers/wyscout.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from typing import Optional, Union

from kloppy._utils import github_resolve_raw_data_url
from kloppy.config import get_config
from kloppy.domain import EventDataset, EventFactory
from kloppy.infra.serializers.event.wyscout import (
Expand All @@ -9,7 +10,6 @@
WyscoutInputs,
)
from kloppy.io import FileLike, open_as_file
from kloppy.utils import github_resolve_raw_data_url


def load(
Expand Down
9 changes: 9 additions & 0 deletions kloppy/_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .deprecation import (
DeprecatedEnumValue,
deprecated,
)
from .docstrings import docstring_inherit_attributes
from .io import Readable, github_resolve_raw_data_url, to_file_object
from .logging import performance_logging
from .optional import import_optional_dependency
from .strings import remove_suffix, to_snake_case
73 changes: 73 additions & 0 deletions kloppy/_utils/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import functools
import inspect
import warnings

string_types = (bytes, str)


def deprecated(reason):
"""
This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted
when the function is used.
"""

if isinstance(reason, string_types):

def decorator(func1):
if inspect.isclass(func1):
fmt1 = "Call to deprecated class {name} ({reason})."
else:
fmt1 = "Call to deprecated function {name} ({reason})."

@functools.wraps(func1)
def new_func1(*args, **kwargs):
warnings.simplefilter("always", DeprecationWarning)
warnings.warn(
fmt1.format(name=func1.__name__, reason=reason),
category=DeprecationWarning,
stacklevel=2,
)
warnings.simplefilter("default", DeprecationWarning)
return func1(*args, **kwargs)

return new_func1

return decorator

elif inspect.isclass(reason) or inspect.isfunction(reason):
func2 = reason

if inspect.isclass(func2):
fmt2 = "Call to deprecated class {name}."
else:
fmt2 = "Call to deprecated function {name}."

@functools.wraps(func2)
def new_func2(*args, **kwargs):
warnings.simplefilter("always", DeprecationWarning)
warnings.warn(
fmt2.format(name=func2.__name__),
category=DeprecationWarning,
stacklevel=2,
)
warnings.simplefilter("default", DeprecationWarning)
return func2(*args, **kwargs)

return new_func2

else:
raise TypeError(repr(type(reason)))


class DeprecatedEnumValue:
def __init__(self, value):
self.value = value

def __get__(self, instance, owner):
warnings.warn(
f"{owner.__name__} is deprecated. Use GoalkeeperActionType instead.",
DeprecationWarning,
stacklevel=2,
)
return self.value
26 changes: 26 additions & 0 deletions kloppy/_utils/docstrings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
def docstring_inherit_attributes(parent):
def inherit(obj):
other_docs, attribute_docs = obj.__doc__.split("Attributes:\n")

own_attributes = [
attribute.strip()
for attribute in attribute_docs.strip().split("\n")
]

parent_attributes = [
attribute.strip()
for attribute in parent.__doc__.split("Attributes:\n")[-1]
.strip()
.split("\n")
]
obj.__doc__ = (
other_docs
+ "Attributes:\n "
+ "\n ".join(parent_attributes)
+ "\n "
+ "\n ".join(own_attributes)
+ "\n"
)
return obj

return inherit
57 changes: 57 additions & 0 deletions kloppy/_utils/io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from io import BytesIO
from typing import BinaryIO, Union
from urllib.parse import quote
from urllib.request import Request, urlopen

Readable = Union[bytes, BinaryIO]


def to_file_object(s: Readable) -> BinaryIO:
if isinstance(s, bytes):
return BytesIO(s)
return s


def github_resolve_raw_data_url(repository: str, branch: str, file: str) -> str:
"""
Resolve a GitHub repository file to its actual raw data URL.

GitHub serves files differently depending on their size:
- Small files are redirected to raw.githubusercontent.com
- Large files (Git LFS) are redirected to media.githubusercontent.com

This function follows the redirect and returns the final URL.

Args:
repository: The repository in the format "owner/repo" (e.g., "metrica-sports/sample-data")
branch: The branch name (e.g., "master", "main")
file: The file path within the repository (e.g., "data/file.csv")

Returns:
The resolved raw data URL

Examples:
>>> github_resolve_raw_data_url(
... repository="metrica-sports/sample-data",
... branch="master",
... file="data/Sample_Game_1/Sample_Game_1_RawTrackingData_Home_Team.csv"
... )
'https://raw.githubusercontent.com/metrica-sports/sample-data/master/data/Sample_Game_1/Sample_Game_1_RawTrackingData_Home_Team.csv'
"""
# Encode the file path properly to handle spaces and special characters
encoded_file = "/".join(quote(part, safe="") for part in file.split("/"))

# Construct the GitHub raw URL
# This URL will redirect to either raw.githubusercontent.com or media.githubusercontent.com
github_url = f"https://github.com/{repository}/raw/refs/heads/{branch}/{encoded_file}"

# Make a HEAD request to follow redirects and get the final URL
req = Request(github_url, method="HEAD")
try:
with urlopen(req) as response:
# The final URL after following redirects
return response.url
except Exception:
# If there's an error, fall back to the standard raw.githubusercontent.com URL
# This ensures backwards compatibility
return f"https://raw.githubusercontent.com/{repository}/{branch}/{encoded_file}"
31 changes: 31 additions & 0 deletions kloppy/_utils/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from contextlib import contextmanager
from logging import Logger
import time
from typing import Optional


@contextmanager
def performance_logging(
description: str,
counter: Optional[int] = None,
logger: Optional[Logger] = None,
):
start = time.time()
try:
yield
finally:
took = (time.time() - start) * 1000
extra = ""
if counter is not None:
extra = f" ({counter / took * 1000:.1f}items/sec)"

unit = "ms"
if took < 0.1:
took *= 1000
unit = "us"

msg = f"{description} took: {took:.2f}{unit} {extra}"
if logger:
logger.info(msg)
else:
print(msg)
Loading
Loading