From 3c937166534ba966d0c3dc149b0a028554ae0c8e Mon Sep 17 00:00:00 2001 From: atom-tr Date: Mon, 9 Sep 2024 16:08:30 +0700 Subject: [PATCH 01/10] docs: upload --- .gitignore | 2 + .readthedocs.yaml | 32 ++++++++ docs/Makefile | 20 +++++ docs/authors.rst | 5 ++ docs/changelog.rst | 1 + docs/conf.py | 28 +++++++ docs/index.rst | 26 ++++++ docs/make.bat | 35 ++++++++ docs/requirements.txt | 2 + docs/usage/advanced.md | 7 ++ docs/usage/configuration.md | 7 ++ docs/usage/installation.rst | 20 +++++ docs/usage/quickstart.rst | 160 ++++++++++++++++++++++++++++++++++++ 13 files changed, 345 insertions(+) create mode 100644 .readthedocs.yaml create mode 100644 docs/Makefile create mode 100644 docs/authors.rst create mode 100644 docs/changelog.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/requirements.txt create mode 100644 docs/usage/advanced.md create mode 100644 docs/usage/configuration.md create mode 100644 docs/usage/installation.rst create mode 100644 docs/usage/quickstart.rst diff --git a/.gitignore b/.gitignore index e7c1b42..bf77608 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ .tox/ dist/ cache/ +docs/_build/ +venv/ \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..243f0cc --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,32 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # You can also specify other tool versions: + # nodejs: "19" + # rust: "1.64" + # golang: "1.19" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 0000000..d856d8d --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1,5 @@ +======= +Credits +======= + +.. include:: ../AUTHORS.txt \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..4d7817a --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGELOG.rst \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..092e13e --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,28 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'pytracking' +copyright = '2024, Resulto' +author = 'Resulto' +release = '0.2.3' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = ["myst_parser"] + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'classic' +html_static_path = ['_static'] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..edea771 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,26 @@ +.. pytracking documentation master file, created by + sphinx-quickstart on Mon Sep 9 15:34:05 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to pytracking's documentation! +====================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + usage/installation + usage/quickstart + usage/configuration + usage/advanced + changelog + authors + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..00281ca --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx +myst-parser \ No newline at end of file diff --git a/docs/usage/advanced.md b/docs/usage/advanced.md new file mode 100644 index 0000000..0cc49bf --- /dev/null +++ b/docs/usage/advanced.md @@ -0,0 +1,7 @@ +# Advanced Usage + +## Custom Webhook Handlers + +You can provide your own webhook handler to process the webhook data. + +To do this, you can subclass the `BaseWebhookHandler` class and implement the `handle_webhook` method. \ No newline at end of file diff --git a/docs/usage/configuration.md b/docs/usage/configuration.md new file mode 100644 index 0000000..dd68120 --- /dev/null +++ b/docs/usage/configuration.md @@ -0,0 +1,7 @@ +# Configuration + +## Django + +## Flask + +## FastAPI \ No newline at end of file diff --git a/docs/usage/installation.rst b/docs/usage/installation.rst new file mode 100644 index 0000000..2e88e91 --- /dev/null +++ b/docs/usage/installation.rst @@ -0,0 +1,20 @@ +Installation +------------ + +You can install pytracking using pip: + +:: + + pip install pytracking + +You can install specific features with extras: + +:: + + pip install pytracking[django,crypto] + +You can also install all features: + +:: + + pip install pytracking[all] \ No newline at end of file diff --git a/docs/usage/quickstart.rst b/docs/usage/quickstart.rst new file mode 100644 index 0000000..7a054de --- /dev/null +++ b/docs/usage/quickstart.rst @@ -0,0 +1,160 @@ +Basic Library Usage +------------------- + +You can generate two kinds of tracking links with pytracking: a link to a +transparent tracking pixel and a link that redirects to another link. + +Encoding +~~~~~~~~ + +You can encode metadata in both kinds of links. For example, you can associate +a customer id with a click tracking link so when the customer clicks on the +link, you'll know exactly which customer clicked on it. + +pylinktracking implements a stateless tracking strategy: all necessary +information can be encoded in the tracking links. You can optionally keep +common settings (e.g., default metadata to associate with all links, webhook +URL) in a separate configuration. + +The information is encoded using url-safe base64 so anyone intercepting your +links, including your customers, could potentially decode the information. You +can optionally encrypt the tracking information (see below). + +Most functions take as a parameter a ``pytracking.Configuration`` +instance that tells how to generate the links. You can also pass the +configuration parameters as ``**kwargs`` argument or can mix both: the kwargs +will override the configuration parameters. + +Decoding +~~~~~~~~ + +Once you get a request from a tracking link, you can use pytracking to decode +the link and get a ``pytracking.TrackingResult`` instance, which contains +information such as the link to redirect to (if it's a click tracking link), +the associated metadata, the webhook URL to notify, etc. + +Basic Library Examples +---------------------- + +Get Open Tracking Link +~~~~~~~~~~~~~~~~~~~~~~ + +:: + + import pytracking + + open_tracking_url = pytracking.get_open_tracking_url( + {"customer_id": 1}, base_open_tracking_url="https://trackingdomain.com/path/", + webhook_url="http://requestb.in/123", include_webhook_url=True) + + # This will produce a URL such as: + # https://trackingdomain.com/path/e30203jhd9239754jh21387293jhf989sda= + + +Get Open Tracking Link with Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + import pytracking + + configuration = pytracking.Configuration( + base_open_tracking_url="https://trackingdomain.com/path/", + webhook_url="http://requestb.in/123", + include_webhook_url=False) + + open_tracking_url = pytracking.get_open_tracking_url( + {"customer_id": 1}, configuration=configuration) + + # This will produce a URL such as: + # https://trackingdomain.com/path/e30203jhd9239754jh21387293jhf989sda= + + +Get Click Tracking Link +~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + import pytracking + + click_tracking_url = pytracking.get_click_tracking_url( + "http://www.example.com/?query=value", {"customer_id": 1}, + base_click_tracking_url="https://trackingdomain.com/path/", + webhook_url="http://requestb.in/123", include_webhook_url=True) + + # This will produce a URL such as: + # https://trackingdomain.com/path/e30203jhd9239754jh21387293jhf989sda= + + +Get Open Tracking Data from URL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + import pytracking + + full_url = "https://trackingdomain.com/path/e30203jhd9239754jh21387293jhf989sda=" + tracking_result = pytracking.get_open_tracking_result( + full_url, base_open_tracking_url="https://trackingdomain.com/path/") + + # Metadata is in tracking_result.metadata + # Webhook URL is in tracking_result.webhook_url + + +Get Click Tracking Data from URL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + import pytracking + + full_url = "https://trackingdomain.com/path/e30203jhd9239754jh21387293jhf989sda=" + tracking_result = pytracking.get_click_tracking_result( + full_url, base_click_tracking_url="https://trackingdomain.com/path/") + + # Metadata is in tracking_result.metadata + # Webhook URL is in tracking_result.webhook_url + # Tracked URL to redirect to is in tracking_result.tracked_url + + +Get a 1x1 transparent PNG pixel +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + import pytracking + + (pixel_byte_string, mime_type) = pytracking.get_open_tracking_pixel() + + + +Encrypting Data +--------------- + +You can encrypt your encoded data to prevent third parties from accessing the +tracking data encoded in your link. + +To use the encryption feature, you must install pytracking with +``pytracking[crypto]``, which uses the `cryptography Python library +`_. + +Encrypting your data slightly increases the length of the generated URL. + +:: + + import pytracking + from cryptography.fernet import Fernet + + key = Fernet.generate_key() + + # Encode + click_tracking_url = pytracking.get_click_tracking_url( + "http://www.example.com/?query=value", {"customer_id": 1}, + base_click_tracking_url="https://trackingdomain.com/path/", + webhook_url="http://requestb.in/123", include_webhook_url=True, + encryption_bytestring_key=key) + + # Decode + tracking_result = pytracking.get_open_tracking_result( + full_url, base_click_tracking_url="https://trackingdomain.com/path/", + encryption_bytestring_key=key) \ No newline at end of file From 5357c36fe06ef6abf02141d9dc562db7e9f747da Mon Sep 17 00:00:00 2001 From: atom-tr Date: Mon, 9 Sep 2024 16:14:27 +0700 Subject: [PATCH 02/10] readthedocs: add requirements --- .readthedocs.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 243f0cc..7dc180b 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -27,6 +27,6 @@ sphinx: # Optional but recommended, declare the Python requirements required # to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -# python: -# install: -# - requirements: docs/requirements.txt \ No newline at end of file +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file From 6a67384c165237d4b9acbd0a9517b17a31f818dd Mon Sep 17 00:00:00 2001 From: atom-tr Date: Tue, 10 Sep 2024 11:55:53 +0700 Subject: [PATCH 03/10] docs: add api by autodocs --- docs/conf.py | 17 ++++-- docs/ex/django.rst | 3 + docs/index.rst | 32 +++++++++- docs/requirements.txt | 4 +- docs/usage/api.md | 29 +++++++++ docs/usage/configuration.md | 7 --- docs/usage/configuration.rst | 112 +++++++++++++++++++++++++++++++++++ docs/usage/installation.md | 43 ++++++++++++++ docs/usage/installation.rst | 20 ------- docs/usage/quickstart.rst | 64 +++++++++++++++++++- 10 files changed, 296 insertions(+), 35 deletions(-) create mode 100644 docs/ex/django.rst create mode 100644 docs/usage/api.md delete mode 100644 docs/usage/configuration.md create mode 100644 docs/usage/configuration.rst create mode 100644 docs/usage/installation.md delete mode 100644 docs/usage/installation.rst diff --git a/docs/conf.py b/docs/conf.py index 092e13e..f9c1161 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,14 +7,23 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'pytracking' -copyright = '2024, Resulto' -author = 'Resulto' +copyright = '2024, Power Go' +author = 'Power Go' release = '0.2.3' +version = release # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ["myst_parser"] +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", +] + +autoclass_content = "init" + templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] @@ -24,5 +33,5 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'classic' +html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] diff --git a/docs/ex/django.rst b/docs/ex/django.rst new file mode 100644 index 0000000..0c01478 --- /dev/null +++ b/docs/ex/django.rst @@ -0,0 +1,3 @@ +======== +Django +======== \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index edea771..86d1726 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,17 +6,45 @@ Welcome to pytracking's documentation! ====================================== +This library provides a set of functions that provide open and click tracking +when sending emails. This is particularly useful if you rely on an Email +Service Provider (ESP) which does not provide open and click tracking. + +The library only provides building blocks and does not handle the actual +sending of email or the serving of tracking pixel and links, but it comes +pretty close to this. + +.. image:: https://github.com/powergo/pytracking/actions/workflows/test.yml/badge.svg + :target: https://github.com/powergo/pytracking/actions/workflows/test.yml + +.. image:: https://img.shields.io/pypi/v/pytracking.svg + :target: https://pypi.python.org/pypi/pytracking + +.. image:: https://img.shields.io/pypi/l/pytracking.svg + +.. image:: https://img.shields.io/pypi/pyversions/pytracking.svg + + +Further reading +~~~~~~~~~~~~~~~ + .. toctree:: - :maxdepth: 2 - :caption: Contents: + :maxdepth: 1 + :caption: Package documentation: usage/installation + usage/api usage/quickstart usage/configuration usage/advanced changelog authors +.. toctree:: + :maxdepth: 1 + :caption: Examples documentation: + + ex/django Indices and tables ================== diff --git a/docs/requirements.txt b/docs/requirements.txt index 00281ca..1c7311f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,4 @@ sphinx -myst-parser \ No newline at end of file +myst-parser +sphinx_rtd_theme>=0.5.0 +django \ No newline at end of file diff --git a/docs/usage/api.md b/docs/usage/api.md new file mode 100644 index 0000000..0d91da1 --- /dev/null +++ b/docs/usage/api.md @@ -0,0 +1,29 @@ +# API + +## pytracking + +```{eval-rst} +.. automodule:: pytracking + :members: +``` + +## pytracking.html + +```{eval-rst} +.. automodule:: pytracking.html + :members: +``` + +## pytracking.webhook + +```{eval-rst} +.. automodule:: pytracking.webhook + :members: +``` + +## pytracking.django + +```{eval-rst} +.. automodule:: pytracking.django + :members: +``` \ No newline at end of file diff --git a/docs/usage/configuration.md b/docs/usage/configuration.md deleted file mode 100644 index dd68120..0000000 --- a/docs/usage/configuration.md +++ /dev/null @@ -1,7 +0,0 @@ -# Configuration - -## Django - -## Flask - -## FastAPI \ No newline at end of file diff --git a/docs/usage/configuration.rst b/docs/usage/configuration.rst new file mode 100644 index 0000000..98aa894 --- /dev/null +++ b/docs/usage/configuration.rst @@ -0,0 +1,112 @@ +============= +Configuration +============= + +Using pytracking with Django +---------------------------- + +pytracking comes with View classes that you can extend and that handle open and +click tracking link request. + +For example, the ``pytracking.django.OpenTrackingView`` will return a 1x1 +transparent PNG pixel for GET requests. The +``pytracking.django.ClickTrackingView`` will return a 302 redirect response to +the tracked URL. + +Both views will return a 404 response if the tracking URL is invalid. Both +views will capture the user agent and the user ip of the request. This +information will be available in TrackingResult.request_data. + +You can extend both views to determine what to do with the tracking result +(e.g., call a webhook or submit a task to a celery queue). Finally, you can +encode your configuration parameters in your Django settings or you can compute +them in your view. + +To use the django feature, you must install pytracking with +``pytracking[django]``. + +Configuration parameters in Django settings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can provide default configuration parameters in your Django settings by +adding this key in your settings file: + +:: + + PYTRACKING_CONFIGURATION = { + "webhook_url": "http://requestb.in/123", + "base_open_tracking_url": "http://tracking.domain.com/open/", + "base_click_tracking_url": "http://tracking.domain.com/click/", + "default_metadata": {"analytics_key": "123456"}, + "append_slash": True + } + + +Extending default views +~~~~~~~~~~~~~~~~~~~~~~~ + +:: + + from pytracking import Configuration + from pytracking.django import OpenTrackingView, ClickTrackingView + + class MyOpenTrackingView(OpenTrackingView): + + def notify_tracking_event(self, tracking_result): + # Override this method to do something with the tracking result. + # tracking_result.request_data["user_agent"] and + # tracking_result.request_data["user_ip"] contains the user agent + # and ip of the client. + send_tracking_result_to_queue(tracking_result) + + def notify_decoding_error(self, exception, request): + # Called when the tracking link cannot be decoded + # Override this to, for example, log the exception + logger.log(exception) + + def get_configuration(self): + # By defaut, fetchs the configuration parameters from the Django + # settings. You can return your own Configuration object here if + # you do not want to use Django settings. + return Configuration() + + + class MyClickTrackingView(ClickTrackingView): + + def notify_tracking_event(self, tracking_result): + # Override this method to do something with the tracking result. + # tracking_result.request_data["user_agent"] and + # tracking_result.request_data["user_ip"] contains the user agent + # and ip of the client. + send_tracking_result_to_queue(tracking_result) + + def notify_decoding_error(self, exception, request): + # Called when the tracking link cannot be decoded + # Override this to, for example, log the exception + logger.log(exception) + + def get_configuration(self): + # By defaut, fetchs the configuration parameters from the Django + # settings. You can return your own Configuration object here if + # you do not want to use Django settings. + return Configuration() + +URLs configuration +~~~~~~~~~~~~~~~~~~ + +Add this to your urls.py file: + +:: + + urlpatterns = [ + url( + "^open/(?P[\w=-]+)/$", MyOpenTrackingView.as_view(), + name="open_tracking"), + url( + "^click/(?P[\w=-]+)/$", MyClickTrackingView.as_view(), + name="click_tracking"), + ] + +.. ## Flask + +.. ## FastAPI \ No newline at end of file diff --git a/docs/usage/installation.md b/docs/usage/installation.md new file mode 100644 index 0000000..16cab3a --- /dev/null +++ b/docs/usage/installation.md @@ -0,0 +1,43 @@ +# Installation + +`pytracking` requires Python 3. + +On Debian or Ubuntu systems, run: + +```text +sudo apt-get install python3-pip +``` + +Python 3 installers for Windows and macOS can be found at + +## Install pytracking + +You can install pytracking using pip: + +```sh +pip install pytracking +``` + +You can install specific features with extras: + +```sh +pip install pytracking[django,crypto] +``` + +You can also install all features: + +```sh +pip install pytracking[all] +``` + +Or, install the latest development release directly from GitHub: + +```sh +pip install git+https://github.com/powergo/pytracking.git +``` + +You can install specific features with extras directly from GitHub: + +```sh +pip3 install -U "git+https://github.com/powergo/pytracking.git@master#egg=pytracking[django,crypto]" +``` \ No newline at end of file diff --git a/docs/usage/installation.rst b/docs/usage/installation.rst deleted file mode 100644 index 2e88e91..0000000 --- a/docs/usage/installation.rst +++ /dev/null @@ -1,20 +0,0 @@ -Installation ------------- - -You can install pytracking using pip: - -:: - - pip install pytracking - -You can install specific features with extras: - -:: - - pip install pytracking[django,crypto] - -You can also install all features: - -:: - - pip install pytracking[all] \ No newline at end of file diff --git a/docs/usage/quickstart.rst b/docs/usage/quickstart.rst index 7a054de..8c9e371 100644 --- a/docs/usage/quickstart.rst +++ b/docs/usage/quickstart.rst @@ -157,4 +157,66 @@ Encrypting your data slightly increases the length of the generated URL. # Decode tracking_result = pytracking.get_open_tracking_result( full_url, base_click_tracking_url="https://trackingdomain.com/path/", - encryption_bytestring_key=key) \ No newline at end of file + encryption_bytestring_key=key) + +Notifying Webhooks +------------------ + +You can send a POST request to a webhook with the tracking result. The webhook +feature just packages the tracking result as a json string in the POST body. It +also sets the content encoding to ``application/json``. + +To use the webhook feature, you must install pytracking with +``pytracking[webhook]``. + + +:: + + import pytracking + from pytracking.webhook import send_webhook + + # Assumes that the webhook url is encoded in the url. + full_url = "https://trackingdomain.com/path/e30203jhd9239754jh21387293jhf989sda=" + tracking_result = pytracking.get_open_tracking_result( + full_url, base_click_tracking_url="https://trackingdomain.com/path/") + + # Will send a POST request with the following json str body: + # { + # "is_open_tracking": False, + # "is_click_tracking": True, + # "metadata": {...}, + # "request_data": None, + # "tracked_url": "http://...", + # "timestamp": 1389177318 + # } + send_webhook(tracking_result) + + + +Modifying HTML emails to add tracking links +------------------------------------------- + +If you have an HTML email, pytracking can update all links with tracking links +and it can also add a transparent tracking pixel at the end of your email. + +To use the HTML feature, you must install pytracking with ``pytracking[html]``, +which uses the `lxml library `_. + +:: + + import pytracking + from pytracking.html import adapt_html + + html_email_text = "..." + new_html_email_text = adapt_html( + html_email_text, extra_metadata={"customer_id": 1}, + click_tracking=True, open_tracking=True) + + +Testing pytracking +------------------ + +pytracking uses `tox `_ and `py.test +`_. If you have tox installed, just run +``tox`` and all possible configurations of pytracking will be tested on Python +3.6-3.9. From 6ecaad424275dcf02c5b9ffbb482c83e4219d1e4 Mon Sep 17 00:00:00 2001 From: atom-tr Date: Tue, 10 Sep 2024 13:25:09 +0700 Subject: [PATCH 04/10] fixed: autodocs do not run --- docs/requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 1c7311f..88f9603 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,6 @@ sphinx myst-parser sphinx_rtd_theme>=0.5.0 -django \ No newline at end of file +# install pytracking from pip for autodoc +django +pytracking==0.2.3 \ No newline at end of file From 3e38ac7dc462eff3a9babe64beaf1a505e7649f8 Mon Sep 17 00:00:00 2001 From: atom-tr Date: Tue, 10 Sep 2024 13:28:02 +0700 Subject: [PATCH 05/10] fixed: No matching distribution found for pytracking==0.2.3 --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 88f9603..2cb5442 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,4 +3,4 @@ myst-parser sphinx_rtd_theme>=0.5.0 # install pytracking from pip for autodoc django -pytracking==0.2.3 \ No newline at end of file +pytracking \ No newline at end of file From 466606d6f6b938bd47cc0c83af0bba0724b940bb Mon Sep 17 00:00:00 2001 From: atom-tr Date: Wed, 11 Sep 2024 09:23:53 +0700 Subject: [PATCH 06/10] docs: require all for auto doc --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 2cb5442..e4db153 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,4 +3,4 @@ myst-parser sphinx_rtd_theme>=0.5.0 # install pytracking from pip for autodoc django -pytracking \ No newline at end of file +pytracking[all] \ No newline at end of file From 05de7391325d1f3298ff1122071e4b9b1ebf1754 Mon Sep 17 00:00:00 2001 From: atom-tr Date: Wed, 11 Sep 2024 09:46:06 +0700 Subject: [PATCH 07/10] refactor: add option pixel_position --- pytracking/html.py | 23 +++++++- pytracking/tracking.py | 125 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 133 insertions(+), 15 deletions(-) diff --git a/pytracking/html.py b/pytracking/html.py index 06518ce..48b3eed 100644 --- a/pytracking/html.py +++ b/pytracking/html.py @@ -49,6 +49,13 @@ def adapt_html( def _replace_links(tree, extra_metadata, configuration): + """ + Replace all links in the HTML tree with tracking links. + + :param tree: The HTML tree to modify + :param extra_metadata: Additional metadata for the tracking URL + :param configuration: Configuration object containing settings + """ for (element, attribute, link, pos) in tree.iterlinks(): if element.tag == "a" and attribute == "href" and _valid_link(link): new_link = get_click_tracking_url( @@ -57,9 +64,23 @@ def _replace_links(tree, extra_metadata, configuration): def _add_tracking_pixel(tree, extra_metadata, configuration): + """ + Add a tracking pixel to the HTML tree. + + :param tree: The HTML tree to modify + :param extra_metadata: Additional metadata for the tracking URL + :param configuration: Configuration object containing settings + """ url = get_open_tracking_url(extra_metadata, configuration) pixel = html.Element("img", {"src": url}) - tree.body.append(pixel) + + if hasattr(tree, 'body'): + if configuration.pixel_position == 'top': + tree.body.insert(0, pixel) + else: + tree.body.append(pixel) + else: + tree.insert(0, pixel) def _valid_link(link): diff --git a/pytracking/tracking.py b/pytracking/tracking.py index f815927..6e4e7b2 100644 --- a/pytracking/tracking.py +++ b/pytracking/tracking.py @@ -28,7 +28,7 @@ def __init__( include_webhook_url=False, base_open_tracking_url=None, base_click_tracking_url=None, default_metadata=None, include_default_metadata=False, encryption_bytestring_key=None, - encoding="utf-8", append_slash=False, **kwargs): + encoding="utf-8", append_slash=False, pixel_position='top', **kwargs): """ :param webhook_url: The webhook to notify when a click or open is @@ -48,6 +48,8 @@ def __init__( :param encryption_bytestring_key: The encryption key given by Fernet. :param encoding: The encoding to use to encode and decode the tracking link. Default to utf-8. + :param pixel_position: The position of the tracking pixel in the HTML. + Can be 'top' or 'bottom'. Default is 'top'. :param kwargs: Other args """ self.webhook_url = webhook_url @@ -61,7 +63,8 @@ def __init__( self.encoding = encoding self.kwargs = kwargs self.encryption_key = None - self.append_slash = False + self.append_slash = append_slash + self.pixel_position = pixel_position self.cache_encryption_key() @@ -83,8 +86,13 @@ def __deepcopy__(self, memo): def merge_with_kwargs(self, kwargs): """ + Merge the current configuration with provided parameters. - :param kwargs: + This method creates a copy of the current configuration and updates it with values + from kwargs for existing attributes. It then updates the encryption key if necessary. + + :param kwargs: A dictionary containing configuration parameters to update. + :return: A new Configuration object with the updated values. """ new_configuration = deepcopy(self) for key, value in kwargs.items(): @@ -97,7 +105,13 @@ def merge_with_kwargs(self, kwargs): return new_configuration def cache_encryption_key(self): - """TODO + """ + Cache the encryption key. + + This method creates a Fernet object from the encryption bytestring key if provided. + Otherwise, it sets the encryption key to None. + + The encryption key is used to encrypt and decrypt data in tracking URLs. """ if self.encryption_bytestring_key: self.encryption_key = Fernet(self.encryption_bytestring_key) @@ -105,10 +119,18 @@ def cache_encryption_key(self): self.encryption_key = None def get_data_to_embed(self, url_to_track, extra_metadata): - """TODO + """ + Prepare data to be embedded in the tracking URL. + + This method constructs a dictionary containing the URL to track (if provided), + metadata (including default and extra metadata), and webhook URL (if configured). - :param url_to_track: - :param meta_data: + :param url_to_track: The URL to be tracked (optional). + :type url_to_track: str or None + :param extra_metadata: Additional metadata to be included. + :type extra_metadata: dict or None + :return: A dictionary containing the data to be embedded in the tracking URL. + :rtype: dict """ data = {} if url_to_track: @@ -129,7 +151,17 @@ def get_data_to_embed(self, url_to_track, extra_metadata): return data def get_url_encoded_data_str(self, data_to_embed): - """TODO + """ + Encode and optionally encrypt the data to be embedded in the tracking URL. + + This method takes the data to be embedded, converts it to a JSON string, + and then either encrypts it (if an encryption key is available) or + encodes it using URL-safe Base64 encoding. + + :param data_to_embed: The data to be encoded and embedded in the URL. + :type data_to_embed: dict + :return: The encoded (and possibly encrypted) data string. + :rtype: str """ json_byte_str = json.dumps(data_to_embed).encode(self.encoding) @@ -143,7 +175,14 @@ def get_url_encoded_data_str(self, data_to_embed): return data_str def get_open_tracking_url_from_data_str(self, data_str): - """TODO + """ + Construct the full open tracking URL from the encoded data string. + + This method constructs the full URL for open tracking by appending the encoded data string + to the base open tracking URL. It also appends a slash if configured. + + :param data_str: The encoded data string to be appended to the base URL. + :type data_str: str """ temp_url = urljoin(self.base_open_tracking_url, data_str) if self.append_slash: @@ -151,7 +190,14 @@ def get_open_tracking_url_from_data_str(self, data_str): return temp_url def get_click_tracking_url_from_data_str(self, data_str): - """TODO + """ + Construct the full click tracking URL from the encoded data string. + + This method constructs the full URL for click tracking by appending the encoded data string + to the base click tracking URL. It also appends a slash if configured. + + :param data_str: The encoded data string to be appended to the base URL. + :type data_str: str """ temp_url = urljoin(self.base_click_tracking_url, data_str) if self.append_slash: @@ -159,12 +205,34 @@ def get_click_tracking_url_from_data_str(self, data_str): return temp_url def get_open_tracking_url(self, extra_metadata): + """ + Generate the full open tracking URL. + + This method constructs the full URL for open tracking by embedding the provided metadata + and other configuration settings into the URL. + + :param extra_metadata: Additional metadata to be included in the URL. + :type extra_metadata: dict or None + :return: The full open tracking URL. + :rtype: str + """ data_to_embed = self.get_data_to_embed(None, extra_metadata) data_str = self.get_url_encoded_data_str(data_to_embed) return self.get_open_tracking_url_from_data_str(data_str) def get_click_tracking_url(self, url_to_track, extra_metadata): - """TODO + """ + Generate the full click tracking URL. + + This method constructs the full URL for click tracking by embedding the provided URL to track, + metadata, and other configuration settings into the URL. + + :param url_to_track: The URL to be tracked. + :type url_to_track: str + :param extra_metadata: Additional metadata to be included in the URL. + :type extra_metadata: dict or None + :return: The full click tracking URL. + :rtype: str """ data_to_embed = self.get_data_to_embed(url_to_track, extra_metadata) data_str = self.get_url_encoded_data_str(data_to_embed) @@ -172,7 +240,20 @@ def get_click_tracking_url(self, url_to_track, extra_metadata): def get_tracking_result( self, encoded_url_path, request_data, is_open): - """TODO + """ + Parse the encoded tracking URL and return the tracking result. + + This method decodes the provided encoded URL path, decrypts it if an encryption key is available, + and then extracts the relevant tracking information such as metadata, webhook URL, and tracked URL. + + :param encoded_url_path: The encoded URL path containing tracking information. + :type encoded_url_path: str + :param request_data: The request data (dict) associated with the client that made the request to the tracking link. + :type request_data: dict or None + :param is_open: Indicates if the URL is for open tracking. + :type is_open: bool + :return: The tracking result containing the parsed information. + :rtype: TrackingResult """ timestamp = int(time.time()) if encoded_url_path.startswith("/"): @@ -209,12 +290,28 @@ def get_tracking_result( ) def get_click_tracking_url_path(self, url): - """TODO + """ + Extract the encoded click tracking URL path from the full URL. + + This method extracts the portion of the URL that contains the encoded click tracking information + by removing the base click tracking URL from the full URL. + + :param url: The full URL containing the encoded click tracking information. + :type url: str + :return: The encoded click tracking URL path. """ return url[len(self.base_click_tracking_url):] def get_open_tracking_url_path(self, url): - """TODO + """ + Extract the encoded open tracking URL path from the full URL. + + This method extracts the portion of the URL that contains the encoded open tracking information + by removing the base open tracking URL from the full URL. + + :param url: The full URL containing the encoded open tracking information. + :type url: str + :return: The encoded open tracking URL path. """ return url[len(self.base_open_tracking_url):] From d122d616c3a775c99e74c1a253226ec1a9489f04 Mon Sep 17 00:00:00 2001 From: atom-tr Date: Wed, 11 Sep 2024 10:00:17 +0700 Subject: [PATCH 08/10] fix: do not replace tracking --- pytracking/html.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pytracking/html.py b/pytracking/html.py index 48b3eed..cbd80eb 100644 --- a/pytracking/html.py +++ b/pytracking/html.py @@ -57,7 +57,7 @@ def _replace_links(tree, extra_metadata, configuration): :param configuration: Configuration object containing settings """ for (element, attribute, link, pos) in tree.iterlinks(): - if element.tag == "a" and attribute == "href" and _valid_link(link): + if element.tag == "a" and attribute == "href" and _valid_link(link, configuration): new_link = get_click_tracking_url( link, extra_metadata, configuration) element.attrib["href"] = new_link @@ -82,7 +82,13 @@ def _add_tracking_pixel(tree, extra_metadata, configuration): else: tree.insert(0, pixel) +_valid_scheme = ["http://", "https://", "//"] -def _valid_link(link): - return link.startswith("http://") or link.startswith("https://") or\ - link.startswith("//") +def _valid_link(link, configuration=None): + """ + Check if a link is valid for click tracking. + """ + is_valid = any(link.startswith(scheme) for scheme in _valid_scheme) + if configuration and configuration.base_click_tracking_url: + is_valid = is_valid and not link.startswith(configuration.base_click_tracking_url) + return is_valid From da662a1f38250e9a21cef8ad52dc71a0aefec65a Mon Sep 17 00:00:00 2001 From: atom-tr Date: Wed, 11 Sep 2024 10:50:31 +0700 Subject: [PATCH 09/10] docs: add docstring and parameter type --- docs/conf.py | 11 ++++++-- docs/requirements.txt | 2 +- pytracking/__init__.py | 36 ++++++++++++++++++-------- pytracking/django.py | 18 +++++++------ pytracking/html.py | 58 ++++++++++++++++++++++-------------------- pytracking/tracking.py | 41 ++++++++++++++--------------- pytracking/webhook.py | 4 +-- 7 files changed, 99 insertions(+), 71 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f9c1161..be45b6a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,8 +9,15 @@ project = 'pytracking' copyright = '2024, Power Go' author = 'Power Go' -release = '0.2.3' -version = release + +import re +with open('../setup.py', 'r') as f: + setup_content = f.read() + version_match = re.search(r"version\s*=\s*['\"]([^'\"]+)['\"]", setup_content) + if version_match: + version = release = version_match.group(1) + else: + version = release = 'unknown' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/requirements.txt b/docs/requirements.txt index e4db153..48159cd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,4 +3,4 @@ myst-parser sphinx_rtd_theme>=0.5.0 # install pytracking from pip for autodoc django -pytracking[all] \ No newline at end of file +-e .[all] \ No newline at end of file diff --git a/pytracking/__init__.py b/pytracking/__init__.py index 49aee36..4959092 100644 --- a/pytracking/__init__.py +++ b/pytracking/__init__.py @@ -1,14 +1,30 @@ from pytracking.tracking import ( - Configuration, TrackingResult, get_click_tracking_result, - get_click_tracking_url, get_open_tracking_url_path, - get_click_tracking_url_path, get_open_tracking_url, - get_open_tracking_result, get_open_tracking_pixel, - TRACKING_PIXEL, PNG_MIME_TYPE, DEFAULT_TIMEOUT_SECONDS) + Configuration, + TrackingResult, + get_click_tracking_result, + get_click_tracking_url, + get_open_tracking_url_path, + get_click_tracking_url_path, + get_open_tracking_url, + get_open_tracking_result, + get_open_tracking_pixel, + TRACKING_PIXEL, + PNG_MIME_TYPE, + DEFAULT_TIMEOUT_SECONDS +) __all__ = [ - "Configuration", "TrackingResult", "get_click_tracking_url", - "get_click_tracking_result", "get_click_tracking_url_path", - "get_open_tracking_url_path", "get_open_tracking_url", - "get_open_tracking_result", "get_open_tracking_pixel", - "TRACKING_PIXEL", "PNG_MIME_TYPE", "DEFAULT_TIMEOUT_SECONDS"] + "Configuration", + "TrackingResult", + "get_click_tracking_url", + "get_click_tracking_result", + "get_click_tracking_url_path", + "get_open_tracking_url_path", + "get_open_tracking_url", + "get_open_tracking_result", + "get_open_tracking_pixel", + "TRACKING_PIXEL", + "PNG_MIME_TYPE", + "DEFAULT_TIMEOUT_SECONDS" +] diff --git a/pytracking/django.py b/pytracking/django.py index 24f8018..775daac 100644 --- a/pytracking/django.py +++ b/pytracking/django.py @@ -1,9 +1,11 @@ +from typing import Dict from django.conf import settings from django.http import ( - HttpResponseRedirect, Http404, HttpResponse) + HttpResponseRedirect, Http404, HttpResponse, HttpRequest) from django.views.generic import View from ipware import get_client_ip from pytracking.tracking import ( + TrackingResult, Configuration, get_configuration, TRACKING_PIXEL, PNG_MIME_TYPE) @@ -13,7 +15,7 @@ class TrackingView(View): Subclasses should override notify_* methods. """ - def notify_tracking_event(self, tracking_result): + def notify_tracking_event(self, tracking_result: TrackingResult): """Called once the tracking link has been decoded, and before responding with a redirect or a tracking pixel. @@ -21,7 +23,7 @@ def notify_tracking_event(self, tracking_result): """ pass - def notify_decoding_error(self, exception, request): + def notify_decoding_error(self, exception: Exception, request: HttpRequest): """Called when a decoding error occurs, and before responding with a 404. @@ -30,7 +32,7 @@ def notify_decoding_error(self, exception, request): """ pass - def get_configuration(self): + def get_configuration(self) -> Configuration: """Returns a Configuration instance built from settings.PYTRACKING_CONFIGURATION. @@ -54,7 +56,7 @@ class ClickTrackingView(TrackingView): If no tracking url is present in the decoded URL, a 404 is returned. """ - def get(self, request, path): + def get(self, request: HttpRequest, path: str): configuration = self.get_configuration() try: @@ -84,7 +86,7 @@ class OpenTrackingView(TrackingView): returned. """ - def get(self, request, path): + def get(self, request: HttpRequest, path: str): configuration = self.get_configuration() try: @@ -99,7 +101,7 @@ def get(self, request, path): return HttpResponse(TRACKING_PIXEL, content_type=PNG_MIME_TYPE) -def get_request_data(request): +def get_request_data(request: HttpRequest) -> Dict[str, str]: """Retrieves the user agent and the ip of the client from the Django request. @@ -122,7 +124,7 @@ def get_configuration_from_settings(settings_name="PYTRACKING_CONFIGURATION"): return get_configuration(None, kwargs) -def get_tracking_result(request, path, is_open, configuration=None, **kwargs): +def get_tracking_result(request: HttpRequest, path: str, is_open: bool, configuration: Configuration = None, **kwargs) -> TrackingResult: """Builds a tracking result from a Django request. :param request: A Django request diff --git a/pytracking/html.py b/pytracking/html.py index cbd80eb..b3e46c8 100644 --- a/pytracking/html.py +++ b/pytracking/html.py @@ -3,6 +3,9 @@ from pytracking.tracking import ( get_configuration, get_open_tracking_url, get_click_tracking_url) +from typing import Dict, Optional +from pytracking.tracking import Configuration + DEFAULT_ATTRIBUTES = { "border": "0", @@ -14,41 +17,40 @@ DOCTYPE = "" -def adapt_html( - html_text, extra_metadata, click_tracking=True, open_tracking=True, - configuration=None, **kwargs): - """Changes an HTML string by replacing links () with tracking - links and by adding a 1x1 transparent pixel just before the closing body - tag. - - :param html_text: The HTML to change (unicode or bytestring). - :param extra_metadata: A dict that can be json-encoded and that will - be encoded in the tracking link. - :param click_tracking: If links () must be changed. - :param open_tracking: If a transparent pixel must be added before the - closing body tag. - :param configuration: An optional Configuration instance. - :param kwargs: Optional configuration parameters. If provided with a - Configuration instance, the kwargs parameters will override the - Configuration parameters. +def adapt_html(html_text: str, extra_metadata: dict, click_tracking: bool = True, open_tracking: bool = True, configuration: Configuration = None, **kwargs) -> str: """ - configuration = get_configuration(configuration, kwargs) + Modify HTML by adding tracking links and a tracking pixel. - tree = html.fromstring(html_text) + Args: + html_text (str): The HTML content to modify. + extra_metadata (dict): Additional data to include in tracking links. + click_tracking (bool): If True, replace links with tracking links. + open_tracking (bool): If True, add a tracking pixel. + configuration (Configuration): Custom configuration settings. + **kwargs: Additional configuration parameters. + + Returns: + str: Modified HTML content with tracking elements. + This function processes the input HTML to add tracking capabilities: + + * Replaces regular links with click-tracking links if click_tracking is True. + * Adds a 1x1 transparent pixel for open tracking if open_tracking is True. + * Uses the provided configuration or creates a new one from kwargs. + """ + configuration = get_configuration(configuration, kwargs) + tree = html.fromstring(html_text) + if click_tracking: _replace_links(tree, extra_metadata, configuration) - + if open_tracking: _add_tracking_pixel(tree, extra_metadata, configuration) - - new_html_text = html.tostring( - tree, include_meta_content_type=True, doctype=DOCTYPE) - - return new_html_text.decode("utf-8") + + return html.tostring(tree, include_meta_content_type=True, doctype=DOCTYPE).decode("utf-8") -def _replace_links(tree, extra_metadata, configuration): +def _replace_links(tree: html.Element, extra_metadata: Dict, configuration: Configuration): """ Replace all links in the HTML tree with tracking links. @@ -63,7 +65,7 @@ def _replace_links(tree, extra_metadata, configuration): element.attrib["href"] = new_link -def _add_tracking_pixel(tree, extra_metadata, configuration): +def _add_tracking_pixel(tree: html.Element, extra_metadata: Dict, configuration: Configuration): """ Add a tracking pixel to the HTML tree. @@ -84,7 +86,7 @@ def _add_tracking_pixel(tree, extra_metadata, configuration): _valid_scheme = ["http://", "https://", "//"] -def _valid_link(link, configuration=None): +def _valid_link(link: str, configuration: Configuration = None) -> bool: """ Check if a link is valid for click tracking. """ diff --git a/pytracking/tracking.py b/pytracking/tracking.py index 6e4e7b2..1d1d1cb 100644 --- a/pytracking/tracking.py +++ b/pytracking/tracking.py @@ -1,6 +1,7 @@ import base64 from collections import namedtuple from copy import deepcopy +from typing import Dict import json import time from urllib.parse import urljoin @@ -23,12 +24,12 @@ class Configuration(object): def __init__( - self, webhook_url=None, - webhook_timeout_seconds=DEFAULT_TIMEOUT_SECONDS, - include_webhook_url=False, base_open_tracking_url=None, - base_click_tracking_url=None, default_metadata=None, - include_default_metadata=False, encryption_bytestring_key=None, - encoding="utf-8", append_slash=False, pixel_position='top', **kwargs): + self, webhook_url: str = None, + webhook_timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, + include_webhook_url: bool = False, base_open_tracking_url: str = None, + base_click_tracking_url: str = None, default_metadata: Dict = None, + include_default_metadata: bool = False, encryption_bytestring_key: str = None, + encoding: str = "utf-8", append_slash: bool = False, pixel_position: str = 'top', **kwargs): """ :param webhook_url: The webhook to notify when a click or open is @@ -150,7 +151,7 @@ def get_data_to_embed(self, url_to_track, extra_metadata): return data - def get_url_encoded_data_str(self, data_to_embed): + def get_url_encoded_data_str(self, data_to_embed: Dict): """ Encode and optionally encrypt the data to be embedded in the tracking URL. @@ -174,7 +175,7 @@ def get_url_encoded_data_str(self, data_to_embed): return data_str - def get_open_tracking_url_from_data_str(self, data_str): + def get_open_tracking_url_from_data_str(self, data_str: str): """ Construct the full open tracking URL from the encoded data string. @@ -189,7 +190,7 @@ def get_open_tracking_url_from_data_str(self, data_str): temp_url += "/" return temp_url - def get_click_tracking_url_from_data_str(self, data_str): + def get_click_tracking_url_from_data_str(self, data_str: str): """ Construct the full click tracking URL from the encoded data string. @@ -204,7 +205,7 @@ def get_click_tracking_url_from_data_str(self, data_str): temp_url += "/" return temp_url - def get_open_tracking_url(self, extra_metadata): + def get_open_tracking_url(self, extra_metadata: Dict): """ Generate the full open tracking URL. @@ -220,7 +221,7 @@ def get_open_tracking_url(self, extra_metadata): data_str = self.get_url_encoded_data_str(data_to_embed) return self.get_open_tracking_url_from_data_str(data_str) - def get_click_tracking_url(self, url_to_track, extra_metadata): + def get_click_tracking_url(self, url_to_track: str, extra_metadata: Dict): """ Generate the full click tracking URL. @@ -239,7 +240,7 @@ def get_click_tracking_url(self, url_to_track, extra_metadata): return self.get_click_tracking_url_from_data_str(data_str) def get_tracking_result( - self, encoded_url_path, request_data, is_open): + self, encoded_url_path: str, request_data: Dict, is_open: bool): """ Parse the encoded tracking URL and return the tracking result. @@ -289,7 +290,7 @@ def get_tracking_result( timestamp=timestamp, ) - def get_click_tracking_url_path(self, url): + def get_click_tracking_url_path(self, url: str): """ Extract the encoded click tracking URL path from the full URL. @@ -302,7 +303,7 @@ def get_click_tracking_url_path(self, url): """ return url[len(self.base_click_tracking_url):] - def get_open_tracking_url_path(self, url): + def get_open_tracking_url_path(self, url: str): """ Extract the encoded open tracking URL path from the full URL. @@ -377,7 +378,7 @@ def get_configuration(configuration, kwargs): return configuration -def get_open_tracking_url(metadata=None, configuration=None, **kwargs): +def get_open_tracking_url(metadata: Dict = None, configuration: Configuration = None, **kwargs) -> str: """Returns a tracking URL encoding the metadata and other information specified in the configuration or kwargs. @@ -401,7 +402,7 @@ def get_open_tracking_pixel(): def get_click_tracking_url( - url_to_track, metadata=None, configuration=None, **kwargs): + url_to_track: str, metadata: Dict = None, configuration: Configuration = None, **kwargs) -> str: """Returns a tracking URL encoding the link to track, the provided metadata, and other information specified in the configuration or kwargs. @@ -419,7 +420,7 @@ def get_click_tracking_url( def get_click_tracking_result( - encoded_url_path, request_data=None, configuration=None, **kwargs): + encoded_url_path: str, request_data: Dict = None, configuration: Configuration = None, **kwargs) -> TrackingResult: """Get a TrackingResult instance from an encoded click tracking link. :param encoded_url_path: The part of the URL that is encoded and contains @@ -444,7 +445,7 @@ def get_click_tracking_result( def get_click_tracking_url_path( - url, configuration=None, **kwargs): + url: str, configuration: Configuration = None, **kwargs) -> str: """Get a part of a URL that contains the encoded click tracking information. This is the part that needs to be supplied to get_click_tracking_result. @@ -460,7 +461,7 @@ def get_click_tracking_url_path( def get_open_tracking_result( - encoded_url_path, request_data=None, configuration=None, **kwargs): + encoded_url_path: str, request_data: Dict = None, configuration: Configuration = None, **kwargs) -> TrackingResult: """Get a TrackingResult instance from an encoded open tracking link. :param encoded_url_path: The part of the URL that is encoded and contains @@ -485,7 +486,7 @@ def get_open_tracking_result( def get_open_tracking_url_path( - url, configuration=None, **kwargs): + url: str, configuration: Configuration = None, **kwargs) -> str: """Get a part of a URL that contains the encoded open tracking information. This is the part that needs to be supplied to get_open_tracking_result. diff --git a/pytracking/webhook.py b/pytracking/webhook.py index 468acf3..6138195 100644 --- a/pytracking/webhook.py +++ b/pytracking/webhook.py @@ -1,9 +1,9 @@ import requests -from pytracking.tracking import get_configuration +from pytracking.tracking import get_configuration, TrackingResult, Configuration -def send_webhook(tracking_result, configuration=None, **kwargs): +def send_webhook(tracking_result: TrackingResult, configuration: Configuration=None, **kwargs): """Sends a POST request to the webhook URL specified in tracking_result. The POST request will have a body of type application/json that contains a From 21a36acf0f558fb8d952ed0fc950a848a81a9ddf Mon Sep 17 00:00:00 2001 From: atom-tr Date: Wed, 11 Sep 2024 10:53:06 +0700 Subject: [PATCH 10/10] add version --- CHANGELOG.rst | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0ff138a..3fe92ef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Changelog - pytracking ====================== +0.2.4 - September 19th 2024 +-------------------------- + +- Added pixel_position configuration parameter to specify where to insert the tracking pixel in the HTML +- Do not replace href in tags that are already tracking URLs + 0.2.3 - November 24th 2022 -------------------------- diff --git a/setup.py b/setup.py index 68fff6a..091097e 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version='0.2.3', + version='0.2.4', description='Email open and click tracking', long_description=long_description,