Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/1.0.3 #369

Merged
merged 6 commits into from
Aug 27, 2024
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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ jobs:
- name: Tests
run: |
pytest --cov=snowplow_tracker --cov-report=xml

- name: MyPy
run: |
python -m pip install mypy
mypy snowplow_tracker --exclude '/test'

- name: Demo
run: |
Expand Down
8 changes: 8 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
Version 1.0.3 (2024-08-27)
--------------------------
Fix docs action (close #367)
Update `on_success` docstring (close #358)
Add py.typed to package (close #360) (Thanks to @edgarrmondragon)
Update typing
Fix `PagePing`, `PageView`, and `StructuredEvent` property getters (close #361)

Version 1.0.2 (2024-02-26)
--------------------------
Add Python 3.12 to CI tests (#356) (Thanks to @edgarrmondragon)
Expand Down
22 changes: 14 additions & 8 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
sphinx
sphinx_rtd_theme
sphinx_copybutton
sphinx_minipres
sphinx_tabs
sphinx_togglebutton>=0.2.0
sphinx-autobuild

sphinx==7.1.2
sphinx_rtd_theme==2.0.0
sphinx_copybutton==0.5.2
sphinx_minipres==0.2.1
sphinx_tabs==3.4.5

sphinx_togglebutton==0.3.2
# Transitive dependency of togglebutton causing:
# https://security.snyk.io/vuln/SNYK-PYTHON-SETUPTOOLS-7448482
setuptools==70.0.0

sphinx-autobuild==2021.3.14
myst_nb>0.8.3
sphinx_rtd_theme_ext_color_contrast
sphinx_rtd_theme_ext_color_contrast==0.3.2
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
author = 'Alex Dean, Paul Boocock, Matus Tomlein, Jack Keene'

# The full version, including alpha/beta/rc tags
release = "1.0.2"
release = "1.0.3"


# -- General configuration ---------------------------------------------------
Expand Down
9 changes: 7 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@

setup(
name="snowplow-tracker",
version="1.0.2",
version="1.0.3",
author=authors_str,
author_email=authors_email_str,
packages=["snowplow_tracker", "snowplow_tracker.test", "snowplow_tracker.events"],
package_data={"snowplow_tracker": ["py.typed"]},
url="http://snowplow.io",
license="Apache License 2.0",
description="Snowplow event tracker for Python. Add analytics to your Python and Django apps, webapps and games",
Expand All @@ -65,5 +66,9 @@
"Programming Language :: Python :: 3.12",
"Operating System :: OS Independent",
],
install_requires=["requests>=2.25.1,<3.0", "typing_extensions>=3.7.4"],
install_requires=[
"requests>=2.25.1,<3.0",
"types-requests>=2.25.1,<3.0",
"typing_extensions>=3.7.4",
],
)
2 changes: 1 addition & 1 deletion snowplow_tracker/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
# language governing permissions and limitations there under.
# """

__version_info__ = (1, 0, 2)
__version_info__ = (1, 0, 3)
__version__ = ".".join(str(x) for x in __version_info__)
__build_version__ = __version__ + ""
2 changes: 1 addition & 1 deletion snowplow_tracker/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from snowplow_tracker import _version, SelfDescribingJson

VERSION = "py-%s" % _version.__version__
DEFAULT_ENCODE_BASE64 = True
DEFAULT_ENCODE_BASE64: bool = True # Type hint required for Python 3.6 MyPy check
BASE_SCHEMA_PATH = "iglu:com.snowplowanalytics.snowplow"
MOBILE_SCHEMA_PATH = "iglu:com.snowplowanalytics.mobile"
SCHEMA_TAG = "jsonschema"
Expand Down
2 changes: 1 addition & 1 deletion snowplow_tracker/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def _get_parameter_name() -> str:
match = _MATCH_FIRST_PARAMETER_REGEX.search(code)
if not match:
return "Unnamed parameter"
return match.groups(0)[0]
return str(match.groups(0)[0])


def _check_form_element(element: Dict[str, Any]) -> bool:
Expand Down
8 changes: 3 additions & 5 deletions snowplow_tracker/emitter_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@ def __init__(
:param batch_size: The maximum number of queued events before the buffer is flushed. Default is 10.
:type batch_size: int | None
:param on_success: Callback executed after every HTTP request in a flush has status code 200
Gets passed the number of events flushed.
Gets passed one argument, an array of dictionaries corresponding to the sent events' payloads
:type on_success: function | None
:param on_failure: Callback executed if at least one HTTP request in a flush has status code other than 200
Gets passed two arguments:
1) The number of events which were successfully sent
2) If method is "post": The unsent data in string form;
If method is "get": An array of dictionaries corresponding to the unsent events' payloads
2) An array of dictionaries corresponding to the unsent events' payloads
:type on_failure: function | None
:param byte_limit: The size event list after reaching which queued events will be flushed
:type byte_limit: int | None
Expand Down Expand Up @@ -105,8 +104,7 @@ def on_failure(self) -> Optional[FailureCallback]:
Callback executed if at least one HTTP request in a flush has status code other than 200
Gets passed two arguments:
1) The number of events which were successfully sent
2) If method is "post": The unsent data in string form;
If method is "get": An array of dictionaries corresponding to the unsent events' payloads
2) An array of dictionaries corresponding to the unsent events' payloads
"""
return self._on_failure

Expand Down
45 changes: 32 additions & 13 deletions snowplow_tracker/emitters.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import threading
import requests
import random
from typing import Optional, Union, Tuple, Dict
from typing import Optional, Union, Tuple, Dict, cast, Callable
from queue import Queue

from snowplow_tracker.self_describing_json import SelfDescribingJson
Expand All @@ -31,6 +31,7 @@
Method,
SuccessCallback,
FailureCallback,
EmitterProtocol,
)
from snowplow_tracker.contracts import one_of
from snowplow_tracker.event_store import EventStore, InMemoryEventStore
Expand All @@ -48,7 +49,20 @@
METHODS = {"get", "post"}


class Emitter(object):
# Unifes the two request methods under one interface
class Requester:
post: Callable
get: Callable

def __init__(self, post: Callable, get: Callable):
# 3.6 MyPy compatibility:
# error: Cannot assign to a method
# https://github.com/python/mypy/issues/2427
setattr(self, "post", post)
setattr(self, "get", get)


class Emitter(EmitterProtocol):
"""
Synchronously send Snowplow events to a Snowplow collector
Supports both GET and POST requests
Expand Down Expand Up @@ -83,13 +97,12 @@ def __init__(
:param batch_size: The maximum number of queued events before the buffer is flushed. Default is 10.
:type batch_size: int | None
:param on_success: Callback executed after every HTTP request in a flush has status code 200
Gets passed the number of events flushed.
Gets passed one argument, an array of dictionaries corresponding to the sent events' payloads
:type on_success: function | None
:param on_failure: Callback executed if at least one HTTP request in a flush has status code other than 200
Gets passed two arguments:
1) The number of events which were successfully sent
2) If method is "post": The unsent data in string form;
If method is "get": An array of dictionaries corresponding to the unsent events' payloads
2) An array of dictionaries corresponding to the unsent events' payloads
:type on_failure: function | None
:param byte_limit: The size event list after reaching which queued events will be flushed
:type byte_limit: int | None
Expand Down Expand Up @@ -151,12 +164,15 @@ def __init__(
self.retry_timer = FlushTimer(emitter=self, repeating=False)

self.max_retry_delay_seconds = max_retry_delay_seconds
self.retry_delay = 0
self.retry_delay: Union[int, float] = 0

self.custom_retry_codes = custom_retry_codes
logger.info("Emitter initialized with endpoint " + self.endpoint)

self.request_method = requests if session is None else session
if session is None:
self.request_method = Requester(post=requests.post, get=requests.get)
else:
self.request_method = Requester(post=session.post, get=session.get)

@staticmethod
def as_collector_uri(
Expand All @@ -183,7 +199,7 @@ def as_collector_uri(

if endpoint.split("://")[0] in PROTOCOLS:
endpoint_arr = endpoint.split("://")
protocol = endpoint_arr[0]
protocol = cast(HttpProtocol, endpoint_arr[0])
endpoint = endpoint_arr[1]

if method == "get":
Expand Down Expand Up @@ -427,6 +443,10 @@ def _cancel_retry_timer(self) -> None:
"""
self.retry_timer.cancel()

# This is only here to satisfy the `EmitterProtocol` interface
def async_flush(self) -> None:
return


class AsyncEmitter(Emitter):
"""
Expand All @@ -446,7 +466,7 @@ def __init__(
byte_limit: Optional[int] = None,
request_timeout: Optional[Union[float, Tuple[float, float]]] = None,
max_retry_delay_seconds: int = 60,
buffer_capacity: int = None,
buffer_capacity: Optional[int] = None,
custom_retry_codes: Dict[int, bool] = {},
event_store: Optional[EventStore] = None,
session: Optional[requests.Session] = None,
Expand All @@ -463,13 +483,12 @@ def __init__(
:param batch_size: The maximum number of queued events before the buffer is flushed. Default is 10.
:type batch_size: int | None
:param on_success: Callback executed after every HTTP request in a flush has status code 200
Gets passed the number of events flushed.
Gets passed one argument, an array of dictionaries corresponding to the sent events' payloads
:type on_success: function | None
:param on_failure: Callback executed if at least one HTTP request in a flush has status code other than 200
Gets passed two arguments:
1) The number of events which were successfully sent
2) If method is "post": The unsent data in string form;
If method is "get": An array of dictionaries corresponding to the unsent events' payloads
2) An array of dictionaries corresponding to the unsent events' payloads
:type on_failure: function | None
:param thread_count: Number of worker threads to use for HTTP requests
:type thread_count: int
Expand Down Expand Up @@ -501,7 +520,7 @@ def __init__(
event_store=event_store,
session=session,
)
self.queue = Queue()
self.queue: Queue = Queue()
for i in range(thread_count):
t = threading.Thread(target=self.consume)
t.daemon = True
Expand Down
11 changes: 6 additions & 5 deletions snowplow_tracker/event_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# language governing permissions and limitations there under.
# """

from typing import List
from typing_extensions import Protocol
from snowplow_tracker.typing import PayloadDict, PayloadDictList
from logging import Logger
Expand All @@ -25,7 +26,7 @@ class EventStore(Protocol):
EventStore protocol. For buffering events in the Emitter.
"""

def add_event(payload: PayloadDict) -> bool:
def add_event(self, payload: PayloadDict) -> bool:
"""
Add PayloadDict to buffer. Returns True if successful.

Expand All @@ -35,15 +36,15 @@ def add_event(payload: PayloadDict) -> bool:
"""
...

def get_events_batch() -> PayloadDictList:
def get_events_batch(self) -> PayloadDictList:
"""
Get a list of all the PayloadDicts in the buffer.

:rtype PayloadDictList
"""
...

def cleanup(batch: PayloadDictList, need_retry: bool) -> None:
def cleanup(self, batch: PayloadDictList, need_retry: bool) -> None:
"""
Removes sent events from the event store. If events need to be retried they are re-added to the buffer.

Expand All @@ -54,7 +55,7 @@ def cleanup(batch: PayloadDictList, need_retry: bool) -> None:
"""
...

def size() -> int:
def size(self) -> int:
"""
Returns the number of events in the buffer

Expand All @@ -76,7 +77,7 @@ def __init__(self, logger: Logger, buffer_capacity: int = 10000) -> None:
When the buffer is full new events are lost.
:type buffer_capacity int
"""
self.event_buffer = []
self.event_buffer: List[PayloadDict] = []
self.buffer_capacity = buffer_capacity
self.logger = logger

Expand Down
5 changes: 2 additions & 3 deletions snowplow_tracker/events/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,9 @@ def build_payload(
if self.event_subject is not None:
fin_payload_dict = self.event_subject.combine_subject(subject)
else:
fin_payload_dict = None if subject is None else subject.standard_nv_pairs
fin_payload_dict = {} if subject is None else subject.standard_nv_pairs

if fin_payload_dict is not None:
self.payload.add_dict(fin_payload_dict)
self.payload.add_dict(fin_payload_dict)
return self.payload

@property
Expand Down
14 changes: 7 additions & 7 deletions snowplow_tracker/events/page_ping.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def page_url(self) -> str:
"""
URL of the viewed page
"""
return self.payload.get("url")
return self.payload.nv_pairs["url"]

@page_url.setter
def page_url(self, value: str):
Expand All @@ -93,7 +93,7 @@ def page_title(self) -> Optional[str]:
"""
URL of the viewed page
"""
return self.payload.get("page")
return self.payload.nv_pairs.get("page")

@page_title.setter
def page_title(self, value: Optional[str]):
Expand All @@ -104,7 +104,7 @@ def referrer(self) -> Optional[str]:
"""
The referrer of the page
"""
return self.payload.get("refr")
return self.payload.nv_pairs.get("refr")

@referrer.setter
def referrer(self, value: Optional[str]):
Expand All @@ -115,7 +115,7 @@ def min_x(self) -> Optional[int]:
"""
Minimum page x offset seen in the last ping period
"""
return self.payload.get("pp_mix")
return self.payload.nv_pairs.get("pp_mix")

@min_x.setter
def min_x(self, value: Optional[int]):
Expand All @@ -126,7 +126,7 @@ def max_x(self) -> Optional[int]:
"""
Maximum page x offset seen in the last ping period
"""
return self.payload.get("pp_max")
return self.payload.nv_pairs.get("pp_max")

@max_x.setter
def max_x(self, value: Optional[int]):
Expand All @@ -137,7 +137,7 @@ def min_y(self) -> Optional[int]:
"""
Minimum page y offset seen in the last ping period
"""
return self.payload.get("pp_miy")
return self.payload.nv_pairs.get("pp_miy")

@min_y.setter
def min_y(self, value: Optional[int]):
Expand All @@ -148,7 +148,7 @@ def max_y(self) -> Optional[int]:
"""
Maximum page y offset seen in the last ping period
"""
return self.payload.get("pp_may")
return self.payload.nv_pairs.get("pp_may")

@max_y.setter
def max_y(self, value: Optional[int]):
Expand Down
Loading
Loading