diff --git a/.annotation_safe_list.yml b/.annotation_safe_list.yml
new file mode 100644
index 0000000..62eaaa7
--- /dev/null
+++ b/.annotation_safe_list.yml
@@ -0,0 +1,41 @@
+# This is a Code Annotations automatically-generated Django model safelist file.
+# These models must be annotated as follows in order to be counted in the coverage report.
+# See https://code-annotations.readthedocs.io/en/latest/safelist.html for more information.
+#
+# fake_app_1.FakeModelName:
+# ".. no_pii:": "This model has no PII"
+# fake_app_2.FakeModel2:
+# ".. choice_annotation:": foo, bar, baz
+
+admin.LogEntry:
+ ".. no_pii:": "This model has no PII"
+auth.Group:
+ ".. no_pii:": "This model has no PII"
+auth.Permission:
+ ".. no_pii:": "This model has no PII"
+auth.User:
+ ".. pii": "This model minimally contains a username, password, and email"
+ ".. pii_types": "username, email_address, password"
+ ".. pii_retirement": "consumer_api"
+contenttypes.ContentType:
+ ".. no_pii:": "This model has no PII"
+sessions.Session:
+ ".. no_pii:": "This model has no PII"
+social_django.Association:
+ ".. no_pii:": "This model has no PII"
+social_django.Code:
+ ".. pii:": "Email address"
+ ".. pii_types:": other
+ ".. pii_retirement:": local_api
+social_django.Nonce:
+ ".. no_pii:": "This model has no PII"
+social_django.Partial:
+ ".. no_pii:": "This model has no PII"
+social_django.UserSocialAuth:
+ ".. no_pii:": "This model has no PII"
+waffle.Flag:
+ ".. no_pii:": "This model has no PII"
+waffle.Sample:
+ ".. no_pii:": "This model has no PII"
+waffle.Switch:
+ ".. no_pii:": "This model has no PII"
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..5dbcfc0
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,10 @@
+[run]
+branch = True
+data_file = .coverage
+source=event_sink_clickhouse
+omit =
+ test_settings
+ *migrations*
+ *admin.py
+ *static*
+ *templates*
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..5cf5aed
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,100 @@
+# ***************************
+# ** DO NOT EDIT THIS FILE **
+# ***************************
+#
+# This file was generated by edx-lint: https://github.com/openedx/edx-lint
+#
+# If you want to change this file, you have two choices, depending on whether
+# you want to make a local change that applies only to this repo, or whether
+# you want to make a central change that applies to all repos using edx-lint.
+#
+# Note: If your .editorconfig file is simply out-of-date relative to the latest
+# .editorconfig in edx-lint, ensure you have the latest edx-lint installed
+# and then follow the steps for a "LOCAL CHANGE".
+#
+# LOCAL CHANGE:
+#
+# 1. Edit the local .editorconfig_tweaks file to add changes just to this
+# repo's file.
+#
+# 2. Run:
+#
+# $ edx_lint write .editorconfig
+#
+# 3. This will modify the local file. Submit a pull request to get it
+# checked in so that others will benefit.
+#
+#
+# CENTRAL CHANGE:
+#
+# 1. Edit the .editorconfig file in the edx-lint repo at
+# https://github.com/openedx/edx-lint/blob/master/edx_lint/files/.editorconfig
+#
+# 2. install the updated version of edx-lint (in edx-lint):
+#
+# $ pip install .
+#
+# 3. Run (in edx-lint):
+#
+# $ edx_lint write .editorconfig
+#
+# 4. Make a new version of edx_lint, submit and review a pull request with the
+# .editorconfig update, and after merging, update the edx-lint version and
+# publish the new version.
+#
+# 5. In your local repo, install the newer version of edx-lint.
+#
+# 6. Run:
+#
+# $ edx_lint write .editorconfig
+#
+# 7. This will modify the local file. Submit a pull request to get it
+# checked in so that others will benefit.
+#
+#
+#
+#
+#
+# STAY AWAY FROM THIS FILE!
+#
+#
+#
+#
+#
+# SERIOUSLY.
+#
+# ------------------------------
+# Generated by edx-lint version: 5.2.5
+# ------------------------------
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 4
+max_line_length = 120
+trim_trailing_whitespace = true
+
+[{Makefile, *.mk}]
+indent_style = tab
+indent_size = 8
+
+[*.{yml,yaml,json}]
+indent_size = 2
+
+[*.js]
+indent_size = 2
+
+[*.diff]
+trim_trailing_whitespace = false
+
+[.git/*]
+trim_trailing_whitespace = false
+
+[COMMIT_EDITMSG]
+max_line_length = 72
+
+[*.rst]
+max_line_length = 79
+
+# f2f02689fced7a2e0c62c2f9803184114dc2ae4b
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..ed62972
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,10 @@
+
+**Merge checklist:**
+Check off if complete *or* not applicable:
+- [ ] Version bumped
+- [ ] Changelog record added
+- [ ] Documentation updated (not only docstrings)
+- [ ] Fixup commits are squashed away
+- [ ] Unit tests added/updated
+- [ ] Manual testing instructions provided
+- [ ] Noted any: Concerns, dependencies, migration issues, deadlines, tickets
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..80cc1d5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,44 @@
+name: Python CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches:
+ - '**'
+
+
+jobs:
+ run_tests:
+ name: tests
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-20.04]
+ python-version: ['3.8']
+ toxenv: [quality, docs, pii_check, django32, django40]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: setup python
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install pip
+ run: pip install -r requirements/pip.txt
+
+ - name: Install Dependencies
+ run: pip install -r requirements/ci.txt
+
+ - name: Run Tests
+ env:
+ TOXENV: ${{ matrix.toxenv }}
+ run: tox
+
+ - name: Run coverage
+ if: matrix.python-version == '3.8' && matrix.toxenv == 'django32'
+ uses: codecov/codecov-action@v3
+ with:
+ flags: unittests
+ fail_ci_if_error: true
diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml
new file mode 100644
index 0000000..fec11d6
--- /dev/null
+++ b/.github/workflows/commitlint.yml
@@ -0,0 +1,10 @@
+# Run commitlint on the commit messages in a pull request.
+
+name: Lint Commit Messages
+
+on:
+ - pull_request
+
+jobs:
+ commitlint:
+ uses: openedx/.github/.github/workflows/commitlint.yml@master
diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml
new file mode 100644
index 0000000..a22e9e6
--- /dev/null
+++ b/.github/workflows/pypi-publish.yml
@@ -0,0 +1,30 @@
+name: Publish package to PyPi
+
+on:
+ release:
+ types: [published]
+
+jobs:
+
+ push:
+ runs-on: ubuntu-20.04
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: setup python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+
+ - name: Install pip
+ run: pip install -r requirements/pip.txt
+
+ - name: Build package
+ run: python setup.py sdist bdist_wheel
+
+ - name: Publish to PyPi
+ uses: pypa/gh-action-pypi-publish@master
+ with:
+ user: __token__
+ password: ${{ secrets.PYPI_UPLOAD_TOKEN }}
diff --git a/.github/workflows/upgrade-python-requirements.yml b/.github/workflows/upgrade-python-requirements.yml
new file mode 100644
index 0000000..ef72b47
--- /dev/null
+++ b/.github/workflows/upgrade-python-requirements.yml
@@ -0,0 +1,27 @@
+name: Upgrade Python Requirements
+
+on:
+ schedule:
+ - cron: "0 0 * * 1"
+ workflow_dispatch:
+ inputs:
+ branch:
+ description: "Target branch against which to create requirements PR"
+ required: true
+ default: 'main'
+
+jobs:
+ call-upgrade-python-requirements-workflow:
+ uses: openedx/.github/.github/workflows/upgrade-python-requirements.yml@master
+ with:
+ branch: ${{ github.event.inputs.branch || 'main' }}
+ # optional parameters below; fill in if you'd like github or email notifications
+ user_reviewers: "bmtcril"
+ # team_reviewers: ""
+ email_address: "bmesick@axim.org"
+ # send_success_notification: false
+ secrets:
+ requirements_bot_github_token: ${{ secrets.REQUIREMENTS_BOT_GITHUB_TOKEN }}
+ requirements_bot_github_email: ${{ secrets.REQUIREMENTS_BOT_GITHUB_EMAIL }}
+ edx_smtp_username: ${{ secrets.EDX_SMTP_USERNAME }}
+ edx_smtp_password: ${{ secrets.EDX_SMTP_PASSWORD }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..095437c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,61 @@
+*.py[cod]
+__pycache__
+.pytest_cache
+
+# C extensions
+*.so
+
+# Packages
+*.egg
+*.egg-info
+/dist
+/build
+/eggs
+/parts
+/bin
+/var
+/sdist
+/develop-eggs
+/.installed.cfg
+/lib
+/lib64
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.cache/
+.pytest_cache/
+.coverage
+.coverage.*
+.tox
+coverage.xml
+htmlcov/
+
+
+
+# The Silver Searcher
+.agignore
+
+# OS X artifacts
+*.DS_Store
+
+# Logging
+log/
+logs/
+chromedriver.log
+ghostdriver.log
+
+# Complexity
+output/*.html
+output/*/index.html
+
+# Sphinx
+docs/_build
+docs/modules.rst
+docs/event_sink_clickhouse.rst
+docs/event_sink_clickhouse.*.rst
+
+# Private requirements
+requirements/private.in
+requirements/private.txt
diff --git a/.pii_annotations.yml b/.pii_annotations.yml
new file mode 100644
index 0000000..7da8f3c
--- /dev/null
+++ b/.pii_annotations.yml
@@ -0,0 +1,35 @@
+source_path: ./
+report_path: pii_report
+safelist_path: .annotation_safe_list.yml
+coverage_target: 100.0
+annotations:
+ ".. no_pii:":
+ "pii_group":
+ - ".. pii:":
+ - ".. pii_types:":
+ choices:
+ - id # Unique identifier for the user which is shared across systems
+ - name # Used for any part of the user's name
+ - username
+ - password
+ - location # Used for any part of any type address or country stored
+ - phone_number # Used for phone or fax numbers
+ - email_address
+ - birth_date # Used for any part of a stored birth date
+ - ip # IP address
+ - external_service # Used for external service ids or links such as social media links or usernames, website links, etc.
+ - biography # Any type of free-form biography field
+ - gender
+ - sex
+ - image
+ - video
+ - other
+ - ".. pii_retirement:":
+ choices:
+ - retained # Intentionally kept for legal reasons
+ - local_api # An API exists in this repository for retiring this information
+ - consumer_api # The data's consumer must implement an API for retiring this information
+ - third_party # A third party API exists to retire this data
+extensions:
+ python:
+ - py
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..a7e2d9b
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+event_sink_clickhouse
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..0f029d2
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,15 @@
+# .readthedocs.yml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ configuration: docs/conf.py
+
+python:
+ version: 3.8
+ install:
+ - requirements: requirements/doc.txt
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..12c711d
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,25 @@
+Change Log
+##########
+
+..
+ All enhancements and patches to event_sink_clickhouse will be documented
+ in this file. It adheres to the structure of https://keepachangelog.com/ ,
+ but in reStructuredText instead of Markdown (for ease of incorporation into
+ Sphinx documentation and the PyPI description).
+
+ This project adheres to Semantic Versioning (https://semver.org/).
+
+.. There should always be an "Unreleased" section for changes pending release.
+
+Unreleased
+**********
+
+*
+
+0.1.0 – 2023-04-24
+**********************************************
+
+Added
+=====
+
+* First release on PyPI.
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..7bc605e
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,674 @@
+
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
+
+EdX Inc. wishes to state, in clarification of the above license terms, that
+any public, independently available web service offered over the network and
+communicating with edX's copyrighted works by any form of inter-service
+communication, including but not limited to Remote Procedure Call (RPC)
+interfaces, is not a work based on our copyrighted work within the meaning
+of the license. "Corresponding Source" of this work, or works based on this
+work, as defined by the terms of this license do not include source code
+files for programs used solely to provide those public, independently
+available web services.
+
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..16b278b
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,6 @@
+include CHANGELOG.rst
+include LICENSE.txt
+include README.rst
+include requirements/base.in
+include requirements/constraints.txt
+recursive-include event_sink_clickhouse *.html *.png *.gif *.js *.css *.jpg *.jpeg *.svg
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..7c5fbc8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,112 @@
+.PHONY: clean compile_translations coverage diff_cover docs dummy_translations \
+ extract_translations fake_translations help pii_check pull_translations push_translations \
+ quality requirements selfcheck test test-all upgrade validate install_transifex_client
+
+.DEFAULT_GOAL := help
+
+# For opening files in a browser. Use like: $(BROWSER)relative/path/to/file.html
+BROWSER := python -m webbrowser file://$(CURDIR)/
+
+help: ## display this help message
+ @echo "Please use \`make ' where is one of"
+ @awk -F ':.*?## ' '/^[a-zA-Z]/ && NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort
+
+clean: ## remove generated byte code, coverage reports, and build artifacts
+ find . -name '__pycache__' -exec rm -rf {} +
+ find . -name '*.pyc' -exec rm -f {} +
+ find . -name '*.pyo' -exec rm -f {} +
+ find . -name '*~' -exec rm -f {} +
+ coverage erase
+ rm -fr build/
+ rm -fr dist/
+ rm -fr *.egg-info
+
+coverage: clean ## generate and view HTML coverage report
+ pytest --cov-report html
+ $(BROWSER)htmlcov/index.html
+
+docs: ## generate Sphinx HTML documentation, including API docs
+ tox -e docs
+ $(BROWSER)docs/_build/html/index.html
+
+# Define PIP_COMPILE_OPTS=-v to get more information during make upgrade.
+PIP_COMPILE = pip-compile --upgrade $(PIP_COMPILE_OPTS)
+
+upgrade: export CUSTOM_COMPILE_COMMAND=make upgrade
+upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in
+ pip install -qr requirements/pip-tools.txt
+ # Make sure to compile files after any other files they include!
+ $(PIP_COMPILE) --allow-unsafe -o requirements/pip.txt requirements/pip.in
+ $(PIP_COMPILE) -o requirements/pip-tools.txt requirements/pip-tools.in
+ pip install -qr requirements/pip.txt
+ pip install -qr requirements/pip-tools.txt
+ $(PIP_COMPILE) -o requirements/base.txt requirements/base.in
+ $(PIP_COMPILE) -o requirements/test.txt requirements/test.in
+ $(PIP_COMPILE) -o requirements/doc.txt requirements/doc.in
+ $(PIP_COMPILE) -o requirements/quality.txt requirements/quality.in
+ $(PIP_COMPILE) -o requirements/ci.txt requirements/ci.in
+ $(PIP_COMPILE) -o requirements/dev.txt requirements/dev.in
+ # Let tox control the Django version for tests
+ sed '/^[dD]jango==/d' requirements/test.txt > requirements/test.tmp
+ mv requirements/test.tmp requirements/test.txt
+
+quality: ## check coding style with pycodestyle and pylint
+ tox -e quality
+
+pii_check: ## check for PII annotations on all Django models
+ tox -e pii_check
+
+piptools: ## install pinned version of pip-compile and pip-sync
+ pip install -r requirements/pip.txt
+ pip install -r requirements/pip-tools.txt
+
+requirements: piptools ## install development environment requirements
+ pip-sync -q requirements/dev.txt requirements/private.*
+
+test: clean ## run tests in the current virtualenv
+ pytest
+
+diff_cover: test ## find diff lines that need test coverage
+ diff-cover coverage.xml
+
+test-all: quality pii_check ## run tests on every supported Python/Django combination
+ tox
+ tox -e docs
+
+validate: quality pii_check test ## run tests and quality checks
+
+selfcheck: ## check that the Makefile is well-formed
+ @echo "The Makefile is well-formed."
+
+## Localization targets
+
+extract_translations: ## extract strings to be translated, outputting .mo files
+ rm -rf docs/_build
+ cd event_sink_clickhouse && ../manage.py makemessages -l en -v1 -d django
+ cd event_sink_clickhouse && ../manage.py makemessages -l en -v1 -d djangojs
+
+compile_translations: ## compile translation files, outputting .po files for each supported language
+ cd event_sink_clickhouse && ../manage.py compilemessages
+
+detect_changed_source_translations:
+ cd event_sink_clickhouse && i18n_tool changed
+
+pull_translations: ## pull translations from Transifex
+ tx pull -af -t --mode reviewed
+
+push_translations: ## push source translation files (.po) from Transifex
+ tx push -s
+
+dummy_translations: ## generate dummy translation (.po) files
+ cd event_sink_clickhouse && i18n_tool dummy
+
+build_dummy_translations: extract_translations dummy_translations compile_translations ## generate and compile dummy translation files
+
+validate_translations: build_dummy_translations detect_changed_source_translations ## validate translations
+
+install_transifex_client: ## Install the Transifex client
+ # Instaling client will skip CHANGELOG and LICENSE files from git changes
+ # so remind the user to commit the change first before installing client.
+ git diff -s --exit-code HEAD || { echo "Please commit changes first."; exit 1; }
+ curl -o- https://raw.githubusercontent.com/transifex/cli/master/install.sh | bash
+ git checkout -- LICENSE README.md ## overwritten by Transifex installer
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..3ad6808
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,189 @@
+Event Sink ClickHouse
+#####################
+
+|pypi-badge| |ci-badge| |codecov-badge| |doc-badge| |pyversions-badge|
+|license-badge| |status-badge|
+
+Purpose
+*******
+
+A listener for `Open edX events`_ to send them to ClickHouse. This project
+acts as a plugin to the Edx Platform, listens for configured Open edX events,
+and sends them to a ClickHouse database for analytics or other processing. This
+is being maintained as part of the Open Analytics Reference System (OARS)
+project.
+
+OARS consumes the data sent to ClickHouse by this plugin as part of data
+enrichment for reporting, or capturing data that otherwise does not fit in
+xAPI.
+
+.. _Open edX events: https://github.com/openedx/openedx-events
+
+Getting Started
+***************
+
+Developing
+==========
+
+One Time Setup
+--------------
+.. code-block::
+
+ # Clone the repository
+ git clone git@github.com:openedx/openedx-event-sink-clickhouse.git
+ cd openedx-event-sink-clickhouse
+
+ # Set up a virtualenv using virtualenvwrapper with the same name as the repo and activate it
+ mkvirtualenv -p python3.8 openedx-event-sink-clickhouse
+
+
+Every time you develop something in this repo
+---------------------------------------------
+.. code-block::
+
+ # Activate the virtualenv
+ workon openedx-event-sink-clickhouse
+
+ # Grab the latest code
+ git checkout main
+ git pull
+
+ # Install/update the dev requirements
+ make requirements
+
+ # Run the tests and quality checks (to verify the status before you make any changes)
+ make validate
+
+ # Make a new branch for your changes
+ git checkout -b /
+
+ # Using your favorite editor, edit the code to make your change.
+ vim ...
+
+ # Run your new tests
+ pytest ./path/to/new/tests
+
+ # Run all the tests and quality checks
+ make validate
+
+ # Commit all your changes
+ git commit ...
+ git push
+
+ # Open a PR and ask for review.
+
+Deploying
+=========
+
+TODO: How can a new user go about deploying this component? Is it just a few
+commands? Is there a larger how-to that should be linked here?
+
+PLACEHOLDER: For details on how to deploy this component, see the `deployment how-to`_
+
+.. _deployment how-to: https://docs.openedx.org/projects/openedx-event-sink-clickhouse/how-tos/how-to-deploy-this-component.html
+
+Getting Help
+************
+
+Documentation
+=============
+
+PLACEHOLDER: Start by going through `the documentation`_. If you need more help see below.
+
+.. _the documentation: https://docs.openedx.org/projects/openedx-event-sink-clickhouse
+
+(TODO: `Set up documentation `_)
+
+More Help
+=========
+
+If you're having trouble, we have discussion forums at
+https://discuss.openedx.org where you can connect with others in the
+community.
+
+Our real-time conversations are on Slack. You can request a `Slack
+invitation`_, then join our `community Slack workspace`_.
+
+For anything non-trivial, the best path is to open an issue in this
+repository with as many details about the issue you are facing as you
+can provide.
+
+https://github.com/openedx/openedx-event-sink-clickhouse/issues
+
+For more information about these options, see the `Getting Help`_ page.
+
+.. _Slack invitation: https://openedx.org/slack
+.. _community Slack workspace: https://openedx.slack.com/
+.. _Getting Help: https://openedx.org/getting-help
+
+License
+*******
+
+The code in this repository is licensed under the AGPL 3.0 unless
+otherwise noted.
+
+Please see `LICENSE.txt `_ for details.
+
+Contributing
+************
+
+Contributions are very welcome.
+Please read `How To Contribute `_ for details.
+
+This project is currently accepting all types of contributions, bug fixes,
+security fixes, maintenance work, or new features. However, please make sure
+to have a discussion about your new feature idea with the maintainers prior to
+beginning development to maximize the chances of your change being accepted.
+You can start a conversation by creating a new issue on this repo summarizing
+your idea.
+
+The Open edX Code of Conduct
+****************************
+
+All community members are expected to follow the `Open edX Code of Conduct`_.
+
+.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
+
+People
+******
+
+The assigned maintainers for this component and other project details may be
+found in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml``
+file in this repo.
+
+.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/openedx-event-sink-clickhouse
+
+Reporting Security Issues
+*************************
+
+Please do not report security issues in public. Please email security@tcril.org.
+
+.. |pypi-badge| image:: https://img.shields.io/pypi/v/openedx-event-sink-clickhouse.svg
+ :target: https://pypi.python.org/pypi/openedx-event-sink-clickhouse/
+ :alt: PyPI
+
+.. |ci-badge| image:: https://github.com/openedx/openedx-event-sink-clickhouse/workflows/Python%20CI/badge.svg?branch=main
+ :target: https://github.com/openedx/openedx-event-sink-clickhouse/actions
+ :alt: CI
+
+.. |codecov-badge| image:: https://codecov.io/github/openedx/openedx-event-sink-clickhouse/coverage.svg?branch=main
+ :target: https://codecov.io/github/openedx/openedx-event-sink-clickhouse?branch=main
+ :alt: Codecov
+
+.. |doc-badge| image:: https://readthedocs.org/projects/openedx-event-sink-clickhouse/badge/?version=latest
+ :target: https://openedx-event-sink-clickhouse.readthedocs.io/en/latest/
+ :alt: Documentation
+
+.. |pyversions-badge| image:: https://img.shields.io/pypi/pyversions/openedx-event-sink-clickhouse.svg
+ :target: https://pypi.python.org/pypi/openedx-event-sink-clickhouse/
+ :alt: Supported Python versions
+
+.. |license-badge| image:: https://img.shields.io/github/license/openedx/openedx-event-sink-clickhouse.svg
+ :target: https://github.com/openedx/openedx-event-sink-clickhouse/blob/main/LICENSE.txt
+ :alt: License
+
+.. TODO: Choose one of the statuses below and remove the other status-badge lines.
+.. |status-badge| image:: https://img.shields.io/badge/Status-Experimental-yellow
+.. .. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen
+.. .. |status-badge| image:: https://img.shields.io/badge/Status-Deprecated-orange
+.. .. |status-badge| image:: https://img.shields.io/badge/Status-Unsupported-red
diff --git a/catalog-info.yaml b/catalog-info.yaml
new file mode 100644
index 0000000..6fb2ae8
--- /dev/null
+++ b/catalog-info.yaml
@@ -0,0 +1,32 @@
+# This file records information about this repo. Its use is described in OEP-55:
+# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
+
+apiVersion: backstage.io/v1alpha1
+kind: ""
+metadata:
+ name: 'openedx_event_sink_clickhouse'
+ description: "A sink for Open edX events to send them to ClickHouse"
+ annotations:
+ # (Optional) Annotation keys and values can be whatever you want.
+ # We use it in Open edX repos to have a comma-separated list of GitHub user
+ # names that might be interested in changes to the architecture of this
+ # component.
+ openedx.org/arch-interest-groups: ""
+spec:
+
+ # (Required) This can be a group(`group:` or a user(`user:`)
+ owner: ""
+
+ # (Required) Acceptable Type Values: service, website, library
+ type: ''
+
+ # (Required) Acceptable Lifecycle Values: experimental, production, deprecated
+ lifecycle: 'experimental'
+
+ # (Optional) The value can be the name of any known component.
+ subcomponentOf: ''
+
+ # (Optional) An array of different components or resources.
+ dependsOn:
+ - ''
+ - ''
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..4da4768
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,12 @@
+coverage:
+ status:
+ project:
+ default:
+ enabled: yes
+ target: auto
+ patch:
+ default:
+ enabled: yes
+ target: 100%
+
+comment: false
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..b18cf4b
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,230 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help
+help:
+ @echo "Please use \`make ' where is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " applehelp to make an Apple Help Book"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " epub3 to make an epub3"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+ @echo " coverage to run coverage check of the documentation (if enabled)"
+ @echo " dummy to check syntax errors of document sources"
+
+.PHONY: clean
+clean:
+ rm -rf $(BUILDDIR)/*
+
+.PHONY: html
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+.PHONY: dirhtml
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+.PHONY: singlehtml
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+.PHONY: pickle
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+.PHONY: json
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+.PHONY: htmlhelp
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+.PHONY: qthelp
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/openedx_event_sink_clickhouse.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/openedx_event_sink_clickhouse.qhc"
+
+.PHONY: applehelp
+applehelp:
+ $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+ @echo
+ @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+ @echo "N.B. You won't be able to view it unless you put it in" \
+ "~/Library/Documentation/Help or install it in your application" \
+ "bundle."
+
+.PHONY: devhelp
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/openedx_event_sink_clickhouse"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/openedx_event_sink_clickhouse"
+ @echo "# devhelp"
+
+.PHONY: epub
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+.PHONY: epub3
+epub3:
+ $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
+ @echo
+ @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
+
+.PHONY: latex
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+.PHONY: latexpdf
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: latexpdfja
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: text
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+.PHONY: man
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+.PHONY: texinfo
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+.PHONY: info
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+.PHONY: gettext
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+.PHONY: changes
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+.PHONY: linkcheck
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+.PHONY: doctest
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+.PHONY: coverage
+coverage:
+ $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+ @echo "Testing of coverage in the sources finished, look at the " \
+ "results in $(BUILDDIR)/coverage/python.txt."
+
+.PHONY: xml
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+.PHONY: pseudoxml
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+
+.PHONY: dummy
+dummy:
+ $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
+ @echo
+ @echo "Build finished. Dummy builder generates no files."
diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css
new file mode 100644
index 0000000..aad8245
--- /dev/null
+++ b/docs/_static/theme_overrides.css
@@ -0,0 +1,10 @@
+/* override table width restrictions */
+.wy-table-responsive table td, .wy-table-responsive table th {
+ /* !important prevents the common CSS stylesheets from
+ overriding this as on RTD they are loaded after this stylesheet */
+ white-space: normal !important;
+}
+
+.wy-table-responsive {
+ overflow: visible !important;
+}
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644
index 0000000..565b052
--- /dev/null
+++ b/docs/changelog.rst
@@ -0,0 +1 @@
+.. include:: ../CHANGELOG.rst
diff --git a/docs/concepts/index.rst b/docs/concepts/index.rst
new file mode 100644
index 0000000..8a2b4bd
--- /dev/null
+++ b/docs/concepts/index.rst
@@ -0,0 +1,2 @@
+Concepts
+########
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..2e44f08
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,547 @@
+# pylint: disable=invalid-name
+"""
+openedx_event_sink_clickhouse documentation build configuration file.
+
+This file is execfile()d with the current directory set to its
+containing dir.
+
+Note that not all possible configuration values are present in this
+autogenerated file.
+
+All configuration values have a default; values that are commented out
+serve to show the default.
+"""
+import os
+import re
+import sys
+from datetime import datetime
+from subprocess import check_call
+
+from django import setup as django_setup
+
+
+def get_version(*file_paths):
+ """
+ Extract the version string from the file.
+
+ Input:
+ - file_paths: relative path fragments to file with
+ version string
+ """
+ filename = os.path.join(os.path.dirname(__file__), *file_paths)
+ version_file = open(filename, encoding="utf8").read()
+ version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
+ if version_match:
+ return version_match.group(1)
+ raise RuntimeError('Unable to find version string.')
+
+
+REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.append(REPO_ROOT)
+
+VERSION = get_version('../event_sink_clickhouse', '__init__.py')
+# Configure Django for autodoc usage
+os.environ['DJANGO_SETTINGS_MODULE'] = 'test_settings'
+django_setup()
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.ifconfig',
+ 'sphinx.ext.napoleon'
+]
+
+# A list of warning types to suppress arbitrary warning messages.
+suppress_warnings = [
+ 'image.nonlocal_uri',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#
+# source_encoding = 'utf-8-sig'
+
+# The top level toctree document.
+top_level_doc = 'index'
+
+# General information about the project.
+project = 'Open edX Event Sink ClickHouse'
+copyright = f'{datetime.now().year}, Axim Collaborative, Inc.' # pylint: disable=redefined-builtin
+author = 'Axim Collaborative, Inc.'
+project_title = 'openedx_event_sink_clickhouse'
+documentation_title = f"{project_title}"
+
+# Set display_github to False if you don't want "edit on Github" button
+html_context = {
+ "display_github": True, # Integrate GitHub
+ "github_user": "edx", # Username
+ "github_repo": 'openedx_event_sink_clickhouse', # Repo name
+ "github_version": "main", # Version
+ "conf_py_path": "/docs/", # Path in the checkout to the docs root
+}
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = VERSION
+# The full version, including alpha/beta/rc tags.
+release = VERSION
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = 'en'
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#
+# today = ''
+#
+# Else, today_fmt is used as the format for a strftime call.
+#
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = [
+ '_build',
+ 'Thumbs.db',
+ '.DS_Store',
+ # This file is intended as a guide for developers browsing the source tree,
+ # not to be rendered into the output docs.
+ 'decisions/README.rst',
+]
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+# keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+
+html_theme = 'sphinx_book_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+html_theme_options = {
+ "repository_url": "https://github.com/openedx/openedx_event_sink_clickhouse",
+ "repository_branch": 'main',
+ "path_to_docs": "docs/",
+ "home_page_in_toc": True,
+ "use_repository_button": True,
+ "use_issues_button": True,
+ "use_edit_page_button": True,
+ # Please don't change unless you know what you're doing.
+ "extra_footer": """
+
+
+
+
+ These works by
+ Axim Collaborative, Inc
+ are licensed under a
+ Creative Commons Attribution-ShareAlike 4.0 International License.
+ """
+}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = []
+
+# The name for this set of Sphinx documents.
+# " v documentation" by default.
+#
+# html_title = 'openedx_event_sink_clickhouse v0.1.0'
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#
+html_logo = "https://logos.openedx.org/open-edx-logo-color.png"
+
+# The name of an image file (relative to this directory) to use as a favicon
+# of the docs. This file should be a Windows icon file (.ico) being 16x16
+# or 32x32
+# pixels large.
+#
+html_favicon = "https://logos.openedx.org/open-edx-favicon.ico"
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#
+# html_extra_path = []
+
+# If not None, a 'Last updated on:' timestamp is inserted at every page
+# bottom, using the given strftime format.
+# The empty string is equivalent to '%b %d, %Y'.
+#
+# html_last_updated_fmt = None
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#
+# html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#
+# html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+#
+# html_domain_indices = True
+
+# If false, no index is generated.
+#
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
+#
+# html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# 'ja' uses this config value.
+# 'zh' user can custom change `jieba` dictionary path.
+#
+# html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#
+# html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = f'{project}doc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_target = f'{project}.tex'
+latex_documents = [
+ (top_level_doc, latex_target, documentation_title,
+ author, 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+#
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#
+# latex_appendices = []
+
+# It false, will not define \strong, \code, itleref, \crossref ... but only
+# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
+# packages.
+#
+# latex_keep_old_macro_names = True
+
+# If false, no module index is generated.
+#
+# latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (top_level_doc, project_title, documentation_title,
+ [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#
+# man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (top_level_doc, project_title, documentation_title,
+ author, project_title, 'A sink for Open edX events to send them to ClickHouse',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+#
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#
+# texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#
+# texinfo_no_detailmenu = False
+
+
+# -- Options for Epub output ----------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = project
+epub_author = author
+epub_publisher = author
+epub_copyright = copyright
+
+# The basename for the epub file. It defaults to the project name.
+# epub_basename = project
+
+# The HTML theme for the epub output. Since the default themes are not
+# optimized for small screen space, using the same theme for HTML and epub
+# output is usually not wise. This defaults to 'epub', a theme designed to save
+# visual space.
+#
+# epub_theme = 'epub'
+
+# The language of the text. It defaults to the language option
+# or 'en' if the language is not set.
+#
+# epub_language = ''
+
+# The scheme of the identifier. Typical schemes are ISBN or URL.
+# epub_scheme = ''
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#
+# epub_identifier = ''
+
+# A unique identification for the text.
+#
+# epub_uid = ''
+
+# A tuple containing the cover image and cover page html template filenames.
+#
+# epub_cover = ()
+
+# A sequence of (type, uri, title) tuples for the guide element of content.opf.
+#
+# epub_guide = ()
+
+# HTML files that should be inserted before the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#
+# epub_pre_files = []
+
+# HTML files that should be inserted after the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#
+# epub_post_files = []
+
+# A list of files that should not be packed into the epub file.
+epub_exclude_files = ['search.html']
+
+# The depth of the table of contents in toc.ncx.
+#
+# epub_tocdepth = 3
+
+# Allow duplicate toc entries.
+#
+# epub_tocdup = True
+
+# Choose between 'default' and 'includehidden'.
+#
+# epub_tocscope = 'default'
+
+# Fix unsupported image types using the Pillow.
+#
+# epub_fix_images = False
+
+# Scale large images.
+#
+# epub_max_image_width = 0
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#
+# epub_show_urls = 'inline'
+
+# If false, no index is generated.
+#
+# epub_use_index = True
+
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {
+ 'python': ('https://docs.python.org/3.8', None),
+ 'django': ('https://docs.djangoproject.com/en/3.2/', 'https://docs.djangoproject.com/en/3.2/_objects/'),
+ 'model_utils': ('https://django-model-utils.readthedocs.io/en/latest/', None),
+}
+
+
+def on_init(app): # pylint: disable=unused-argument
+ """
+ Run sphinx-apidoc after Sphinx initialization.
+
+ Read the Docs won't run tox or custom shell commands, so we need this to
+ avoid checking in the generated reStructuredText files.
+ """
+ docs_path = os.path.abspath(os.path.dirname(__file__))
+ root_path = os.path.abspath(os.path.join(docs_path, '..'))
+ apidoc_path = 'sphinx-apidoc'
+ if hasattr(sys, 'real_prefix'): # Check to see if we are in a virtualenv
+ # If we are, assemble the path manually
+ bin_path = os.path.abspath(os.path.join(sys.prefix, 'bin'))
+ apidoc_path = os.path.join(bin_path, apidoc_path)
+ check_call([apidoc_path, '-o', docs_path, os.path.join(root_path, 'event_sink_clickhouse'),
+ os.path.join(root_path, 'event_sink_clickhouse/migrations')])
+
+
+def setup(app):
+ """Sphinx extension: run sphinx-apidoc."""
+ event = 'builder-inited'
+ app.connect(event, on_init)
diff --git a/docs/decisions.rst b/docs/decisions.rst
new file mode 100644
index 0000000..a21903c
--- /dev/null
+++ b/docs/decisions.rst
@@ -0,0 +1,12 @@
+Decisions
+#########
+
+The following `ADRs` are a record of all decisions made as a part of developing this library.
+
+.. _ADRs: https://open-edx-proposals.readthedocs.io/en/latest/oep-0019-bp-developer-documentation.html#adrs
+
+.. toctree::
+ :maxdepth: 1
+ :glob:
+
+ decisions/*
diff --git a/docs/decisions/0001-purpose-of-this-repo.rst b/docs/decisions/0001-purpose-of-this-repo.rst
new file mode 100644
index 0000000..24b6421
--- /dev/null
+++ b/docs/decisions/0001-purpose-of-this-repo.rst
@@ -0,0 +1,43 @@
+0001 Purpose of This Repo
+#########################
+
+Status
+******
+
+Accepted
+
+Context
+*******
+
+While developing the Open Analytics Reference System (OARS), the team found
+a need to get certain information from the CMS into the analytics database.
+This was found to be a common use case, and the design decisions around
+edx-platform plugins and openedx-events made this kind of event sink an obvious
+choice for moving data between systems.
+
+Decision
+********
+
+We will create a repository to house an edx-platform plugin that can listen to
+openedx-events and send them to ClickHouse, as well as working as priot art for
+other future event sinks to different backends.
+
+Consequences
+************
+
+A new repository will be created and maintained to house the code for this
+project. ClickHouse will be able to receive event data from LMS and CMS. This
+plugin will need to be installed to facilitate transfer of data in support of
+OARS.
+
+Rejected Alternatives
+*********************
+
+Originally the idea was to extend the existing `coursegraph`_ functionality
+built into edx-platform and add a second backend for ClickHouse. This solved
+the OARS use case, but locked out older named releases from using that
+functionality and would have to be replicated for each individual type of
+information we wanted to send in the future. It makes more sense to just have
+one plugin that can be expanded to listen for more events.
+
+.. _coursegraph: https://github.com/openedx/edx-platform/tree/master/cms/djangoapps/coursegraph
diff --git a/docs/decisions/README.rst b/docs/decisions/README.rst
new file mode 100644
index 0000000..612ad3b
--- /dev/null
+++ b/docs/decisions/README.rst
@@ -0,0 +1,8 @@
+This directory is a historical record on the architectural decisions we make with this repository as it evolves over time.
+
+It uses Architecture Decision Records, as described by Michael Nygard in `Documenting Architecture Decisions`_.
+
+For more information, see `OEP-19's section on ADRs`_.
+
+.. _Documenting Architecture Decisions: https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions
+.. _OEP-19's section on ADRs: https://open-edx-proposals.readthedocs.io/en/latest/best-practices/oep-0019-bp-developer-documentation.html#adrs
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
new file mode 100644
index 0000000..cec5418
--- /dev/null
+++ b/docs/getting_started.rst
@@ -0,0 +1,18 @@
+Getting Started
+###############
+
+If you have not already done so, create/activate a `virtualenv`_. Unless otherwise stated, assume all terminal code
+below is executed within the virtualenv.
+
+.. _virtualenv: https://virtualenvwrapper.readthedocs.org/en/latest/
+
+
+Install dependencies
+********************
+Dependencies can be installed via the command below.
+
+.. code-block:: bash
+
+ $ make requirements
+
+
diff --git a/docs/how-tos/index.rst b/docs/how-tos/index.rst
new file mode 100644
index 0000000..5147f80
--- /dev/null
+++ b/docs/how-tos/index.rst
@@ -0,0 +1,2 @@
+How-tos
+#######
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..a6fee9d
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,34 @@
+.. openedx_event_sink_clickhouse documentation top level file, created by
+ sphinx-quickstart on Mon Apr 24 11:08:00 2023.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+Open edX Event Sink ClickHouse
+==============================
+
+A sink for Open edX events to broker them them to a ClickHouse database.
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+ readme
+ getting_started
+ quickstarts/index
+ concepts/index
+ how-tos/index
+ testing
+ internationalization
+ modules
+ changelog
+ decisions
+ references/index
+
+
+Indices and tables
+##################
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/internationalization.rst b/docs/internationalization.rst
new file mode 100644
index 0000000..9b25877
--- /dev/null
+++ b/docs/internationalization.rst
@@ -0,0 +1,51 @@
+.. _chapter-i18n:
+
+Internationalization
+####################
+All user-facing text content should be marked for translation. Even if this application is only run in English, our
+open source users may choose to use another language. Marking content for translation ensures our users have
+this choice.
+
+Follow the `internationalization coding guidelines`_ in the edX Developer's Guide when developing new features.
+
+.. _internationalization coding guidelines: https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/internationalization/i18n.html
+
+Updating Translations
+*********************
+This project uses `Transifex`_ to translate content. After new features are developed the translation source files
+should be pushed to Transifex. Our translation community will translate the content, after which we can retrieve the
+translations.
+
+.. _Transifex: https://www.transifex.com/
+
+Pushing source translation files to Transifex requires access to the edx-platform. Request access from the Open Source
+Team if you will be pushing translation files. You should also `configure the Transifex client`_ if you have not done so
+already.
+
+.. _configure the Transifex client: https://docs.transifex.com/client/config/
+
+The `make` targets listed below can be used to push or pull translations.
+
+.. list-table::
+ :widths: 25 75
+ :header-rows: 1
+
+ * - Target
+ - Description
+ * - pull_translations
+ - Pull translations from Transifex
+ * - push_translations
+ - Push source translation files to Transifex
+
+Fake Translations
+*****************
+As you develop features it may be helpful to know which strings have been marked for translation, and which are not.
+Use the `fake_translations` make target for this purpose. This target will extract all strings marked for translation,
+generate fake translations in the Esperanto (eo) language directory, and compile the translations.
+
+You can trigger the display of the translations by setting your browser's language to Esperanto (eo), and navigating to
+a page on the site. Instead of plain English strings, you should see specially-accented English strings that look
+like this:
+
+ Thé Fütüré øf Ønlïné Édüçätïøn Ⱡσяєм ι# Før änýøné, änýwhéré, änýtïmé Ⱡσяєм #
+
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..cbeb5c4
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,281 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^` where ^ is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. epub3 to make an epub3
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ echo. coverage to run coverage check of the documentation if enabled
+ echo. dummy to check syntax errors of document sources
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+REM Check if sphinx-build is available and fallback to Python version if any
+%SPHINXBUILD% 1>NUL 2>NUL
+if errorlevel 9009 goto sphinx_python
+goto sphinx_ok
+
+:sphinx_python
+
+set SPHINXBUILD=python -m sphinx.__init__
+%SPHINXBUILD% 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://sphinx-doc.org/
+ exit /b 1
+)
+
+:sphinx_ok
+
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\openedx_event_sink_clickhouse.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\openedx_event_sink_clickhouse.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "epub3" (
+ %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %~dp0
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %~dp0
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "coverage" (
+ %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of coverage in the sources finished, look at the ^
+results in %BUILDDIR%/coverage/python.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+if "%1" == "dummy" (
+ %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. Dummy builder generates no files.
+ goto end
+)
+
+:end
diff --git a/docs/quickstarts/index.rst b/docs/quickstarts/index.rst
new file mode 100644
index 0000000..e3f408f
--- /dev/null
+++ b/docs/quickstarts/index.rst
@@ -0,0 +1,2 @@
+Quick Start
+###########
diff --git a/docs/readme.rst b/docs/readme.rst
new file mode 100644
index 0000000..72a3355
--- /dev/null
+++ b/docs/readme.rst
@@ -0,0 +1 @@
+.. include:: ../README.rst
diff --git a/docs/references/index.rst b/docs/references/index.rst
new file mode 100644
index 0000000..ba5ea57
--- /dev/null
+++ b/docs/references/index.rst
@@ -0,0 +1,2 @@
+References
+##########
diff --git a/docs/testing.rst b/docs/testing.rst
new file mode 100644
index 0000000..bd25721
--- /dev/null
+++ b/docs/testing.rst
@@ -0,0 +1,44 @@
+.. _chapter-testing:
+
+Testing
+#######
+
+openedx_event_sink_clickhouse has an assortment of test cases and code quality
+checks to catch potential problems during development. To run them all in the
+version of Python you chose for your virtualenv:
+
+.. code-block:: bash
+
+ $ make validate
+
+To run just the unit tests:
+
+.. code-block:: bash
+
+ $ make test
+
+To run just the unit tests and check diff coverage
+
+.. code-block:: bash
+
+ $ make diff_cover
+
+To run just the code quality checks:
+
+.. code-block:: bash
+
+ $ make quality
+
+To run the unit tests under every supported Python version and the code
+quality checks:
+
+.. code-block:: bash
+
+ $ make test-all
+
+To generate and open an HTML report of how much of the code is covered by
+test cases:
+
+.. code-block:: bash
+
+ $ make coverage
diff --git a/event_sink_clickhouse/__init__.py b/event_sink_clickhouse/__init__.py
new file mode 100644
index 0000000..eae5234
--- /dev/null
+++ b/event_sink_clickhouse/__init__.py
@@ -0,0 +1,5 @@
+"""
+A sink for Open edX events to send them to ClickHouse.
+"""
+
+__version__ = '0.1.0'
diff --git a/event_sink_clickhouse/apps.py b/event_sink_clickhouse/apps.py
new file mode 100644
index 0000000..4981b8f
--- /dev/null
+++ b/event_sink_clickhouse/apps.py
@@ -0,0 +1,13 @@
+"""
+event_sink_clickhouse Django application initialization.
+"""
+
+from django.apps import AppConfig
+
+
+class EventSinkClickhouseConfig(AppConfig):
+ """
+ Configuration for the event_sink_clickhouse Django application.
+ """
+
+ name = 'event_sink_clickhouse'
diff --git a/event_sink_clickhouse/conf/locale/config.yaml b/event_sink_clickhouse/conf/locale/config.yaml
new file mode 100644
index 0000000..d1c618e
--- /dev/null
+++ b/event_sink_clickhouse/conf/locale/config.yaml
@@ -0,0 +1,85 @@
+# Configuration for i18n workflow.
+
+locales:
+ - en # English - Source Language
+ - am # Amharic
+ - ar # Arabic
+ - az # Azerbaijani
+ - bg_BG # Bulgarian (Bulgaria)
+ - bn_BD # Bengali (Bangladesh)
+ - bn_IN # Bengali (India)
+ - bs # Bosnian
+ - ca # Catalan
+ - ca@valencia # Catalan (Valencia)
+ - cs # Czech
+ - cy # Welsh
+ - da # Danish
+ - de_DE # German (Germany)
+ - el # Greek
+ - en # English
+ - en_GB # English (United Kingdom)
+ # Don't pull these until we figure out why pages randomly display in these locales,
+ # when the user's browser is in English and the user is not logged in.
+ # - en@lolcat # LOLCAT English
+ # - en@pirate # Pirate English
+ - es_419 # Spanish (Latin America)
+ - es_AR # Spanish (Argentina)
+ - es_EC # Spanish (Ecuador)
+ - es_ES # Spanish (Spain)
+ - es_MX # Spanish (Mexico)
+ - es_PE # Spanish (Peru)
+ - et_EE # Estonian (Estonia)
+ - eu_ES # Basque (Spain)
+ - fa # Persian
+ - fa_IR # Persian (Iran)
+ - fi_FI # Finnish (Finland)
+ - fil # Filipino
+ - fr # French
+ - gl # Galician
+ - gu # Gujarati
+ - he # Hebrew
+ - hi # Hindi
+ - hr # Croatian
+ - hu # Hungarian
+ - hy_AM # Armenian (Armenia)
+ - id # Indonesian
+ - it_IT # Italian (Italy)
+ - ja_JP # Japanese (Japan)
+ - kk_KZ # Kazakh (Kazakhstan)
+ - km_KH # Khmer (Cambodia)
+ - kn # Kannada
+ - ko_KR # Korean (Korea)
+ - lt_LT # Lithuanian (Lithuania)
+ - ml # Malayalam
+ - mn # Mongolian
+ - mr # Marathi
+ - ms # Malay
+ - nb # Norwegian Bokmål
+ - ne # Nepali
+ - nl_NL # Dutch (Netherlands)
+ - or # Oriya
+ - pl # Polish
+ - pt_BR # Portuguese (Brazil)
+ - pt_PT # Portuguese (Portugal)
+ - ro # Romanian
+ - ru # Russian
+ - si # Sinhala
+ - sk # Slovak
+ - sl # Slovenian
+ - sq # Albanian
+ - sr # Serbian
+ - ta # Tamil
+ - te # Telugu
+ - th # Thai
+ - tr_TR # Turkish (Turkey)
+ - uk # Ukranian
+ - ur # Urdu
+ - uz # Uzbek
+ - vi # Vietnamese
+ - zh_CN # Chinese (China)
+ - zh_HK # Chinese (Hong Kong)
+ - zh_TW # Chinese (Taiwan)
+
+# The locales used for fake-accented English, for testing.
+dummy_locales:
+ - eo
diff --git a/event_sink_clickhouse/models.py b/event_sink_clickhouse/models.py
new file mode 100644
index 0000000..30c0848
--- /dev/null
+++ b/event_sink_clickhouse/models.py
@@ -0,0 +1,3 @@
+"""
+Database models for event_sink_clickhouse.
+"""
diff --git a/event_sink_clickhouse/templates/event_sink_clickhouse/base.html b/event_sink_clickhouse/templates/event_sink_clickhouse/base.html
new file mode 100644
index 0000000..d13a13e
--- /dev/null
+++ b/event_sink_clickhouse/templates/event_sink_clickhouse/base.html
@@ -0,0 +1,22 @@
+
+{% comment %}
+As the developer of this package, don't place anything here if you can help it
+since this allows developers to have interoperability between your template
+structure and their own.
+
+Example: Developer melding the 2SoD pattern to fit inside with another pattern::
+
+ {% extends "base.html" %}
+ {% load static %}
+
+
+ {% block extra_js %}
+
+
+ {% block javascript %}
+
+ {% endblock javascript %}
+
+ {% endblock extra_js %}
+{% endcomment %}
+
diff --git a/event_sink_clickhouse/urls.py b/event_sink_clickhouse/urls.py
new file mode 100644
index 0000000..76e1338
--- /dev/null
+++ b/event_sink_clickhouse/urls.py
@@ -0,0 +1,10 @@
+"""
+URLs for event_sink_clickhouse.
+"""
+from django.urls import re_path # pylint: disable=unused-import
+from django.views.generic import TemplateView # pylint: disable=unused-import
+
+urlpatterns = [
+ # TODO: Fill in URL patterns and views here.
+ # re_path(r'', TemplateView.as_view(template_name="event_sink_clickhouse/base.html")),
+]
diff --git a/manage.py b/manage.py
new file mode 100644
index 0000000..c4c1f40
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+"""
+Django administration utility.
+"""
+
+import os
+import sys
+
+PWD = os.path.abspath(os.path.dirname(__file__))
+
+if __name__ == '__main__':
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_settings')
+ sys.path.append(PWD)
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError:
+ # The above import may fail for some other reason. Ensure that the
+ # issue is really that Django is missing to avoid masking other
+ # exceptions on Python 2.
+ try:
+ import django # pylint: disable=unused-import
+ except ImportError as import_error:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from import_error
+ raise
+ execute_from_command_line(sys.argv)
diff --git a/openedx.yaml b/openedx.yaml
new file mode 100644
index 0000000..2785759
--- /dev/null
+++ b/openedx.yaml
@@ -0,0 +1,23 @@
+# This file describes this Open edX repo, as described in OEP-2:
+# https://open-edx-proposals.readthedocs.io/en/latest/oep-0002-bp-repo-metadata.html#specification
+
+---
+oeps:
+ oep-2: true
+ oep-3:
+ state: false
+ reason: TODO - Implement for this application if appropriate
+ oep-5:
+ state: false
+ reason: TODO - Implement for this application if appropriate
+ oep-18: true
+ oep-30:
+ state: true
+openedx-release:
+ # The openedx-release key is described in OEP-10:
+ # https://open-edx-proposals.readthedocs.io/en/latest/oep-0010-proc-openedx-releases.html
+ # The FAQ might also be helpful: https://openedx.atlassian.net/wiki/spaces/COMM/pages/1331268879/Open+edX+Release+FAQ
+ # Note: This will only work if the repo is in the `openedx` org in github. Repos in other orgs that have this
+ # setting will still be treated as if they don't want to be part of the Open edX releases.
+ maybe: true # Delete this "maybe" line when you have decided about Open edX inclusion.
+ ref: main
diff --git a/pii_report/2023-24-04-10-29-05.yaml b/pii_report/2023-24-04-10-29-05.yaml
new file mode 100644
index 0000000..e9c88aa
--- /dev/null
+++ b/pii_report/2023-24-04-10-29-05.yaml
@@ -0,0 +1,79 @@
+.annotation_safe_list.yml:
+- annotation_data: This model has no PII
+ annotation_token: '.. no_pii:'
+ extra:
+ full_comment: '{''.. no_pii:'': ''This model has no PII''}'
+ object_id: contenttypes.ContentType
+ filename: .annotation_safe_list.yml
+ found_by: safelist
+ line_number: 0
+ report_group_id: 1
+- annotation_data: This model has no PII
+ annotation_token: '.. no_pii:'
+ extra:
+ full_comment: '{''.. no_pii:'': ''This model has no PII''}'
+ object_id: admin.LogEntry
+ filename: .annotation_safe_list.yml
+ found_by: safelist
+ line_number: 0
+ report_group_id: 2
+- annotation_data: This model has no PII
+ annotation_token: '.. no_pii:'
+ extra:
+ full_comment: '{''.. no_pii:'': ''This model has no PII''}'
+ object_id: auth.Permission
+ filename: .annotation_safe_list.yml
+ found_by: safelist
+ line_number: 0
+ report_group_id: 3
+- annotation_data: This model has no PII
+ annotation_token: '.. no_pii:'
+ extra:
+ full_comment: '{''.. no_pii:'': ''This model has no PII''}'
+ object_id: sessions.Session
+ filename: .annotation_safe_list.yml
+ found_by: safelist
+ line_number: 0
+ report_group_id: 4
+- annotation_data: This model minimally contains a username, password, and email
+ annotation_token: .. pii
+ extra:
+ full_comment: '{''.. pii'': ''This model minimally contains a username, password,
+ and email'', ''.. pii_types'': ''username, email_address, password'', ''.. pii_retirement'':
+ ''consumer_api''}'
+ object_id: auth.User
+ filename: .annotation_safe_list.yml
+ found_by: safelist
+ line_number: 0
+ report_group_id: 5
+- annotation_data: username, email_address, password
+ annotation_token: .. pii_types
+ extra:
+ full_comment: '{''.. pii'': ''This model minimally contains a username, password,
+ and email'', ''.. pii_types'': ''username, email_address, password'', ''.. pii_retirement'':
+ ''consumer_api''}'
+ object_id: auth.User
+ filename: .annotation_safe_list.yml
+ found_by: safelist
+ line_number: 0
+ report_group_id: 5
+- annotation_data: consumer_api
+ annotation_token: .. pii_retirement
+ extra:
+ full_comment: '{''.. pii'': ''This model minimally contains a username, password,
+ and email'', ''.. pii_types'': ''username, email_address, password'', ''.. pii_retirement'':
+ ''consumer_api''}'
+ object_id: auth.User
+ filename: .annotation_safe_list.yml
+ found_by: safelist
+ line_number: 0
+ report_group_id: 5
+- annotation_data: This model has no PII
+ annotation_token: '.. no_pii:'
+ extra:
+ full_comment: '{''.. no_pii:'': ''This model has no PII''}'
+ object_id: auth.Group
+ filename: .annotation_safe_list.yml
+ found_by: safelist
+ line_number: 0
+ report_group_id: 6
diff --git a/pylintrc b/pylintrc
new file mode 100644
index 0000000..4e9dcce
--- /dev/null
+++ b/pylintrc
@@ -0,0 +1,390 @@
+# ***************************
+# ** DO NOT EDIT THIS FILE **
+# ***************************
+#
+# This file was generated by edx-lint: https://github.com/openedx/edx-lint
+#
+# If you want to change this file, you have two choices, depending on whether
+# you want to make a local change that applies only to this repo, or whether
+# you want to make a central change that applies to all repos using edx-lint.
+#
+# Note: If your pylintrc file is simply out-of-date relative to the latest
+# pylintrc in edx-lint, ensure you have the latest edx-lint installed
+# and then follow the steps for a "LOCAL CHANGE".
+#
+# LOCAL CHANGE:
+#
+# 1. Edit the local pylintrc_tweaks file to add changes just to this
+# repo's file.
+#
+# 2. Run:
+#
+# $ edx_lint write pylintrc
+#
+# 3. This will modify the local file. Submit a pull request to get it
+# checked in so that others will benefit.
+#
+#
+# CENTRAL CHANGE:
+#
+# 1. Edit the pylintrc file in the edx-lint repo at
+# https://github.com/openedx/edx-lint/blob/master/edx_lint/files/pylintrc
+#
+# 2. install the updated version of edx-lint (in edx-lint):
+#
+# $ pip install .
+#
+# 3. Run (in edx-lint):
+#
+# $ edx_lint write pylintrc
+#
+# 4. Make a new version of edx_lint, submit and review a pull request with the
+# pylintrc update, and after merging, update the edx-lint version and
+# publish the new version.
+#
+# 5. In your local repo, install the newer version of edx-lint.
+#
+# 6. Run:
+#
+# $ edx_lint write pylintrc
+#
+# 7. This will modify the local file. Submit a pull request to get it
+# checked in so that others will benefit.
+#
+#
+#
+#
+#
+# STAY AWAY FROM THIS FILE!
+#
+#
+#
+#
+#
+# SERIOUSLY.
+#
+# ------------------------------
+# Generated by edx-lint version: 5.3.4
+# ------------------------------
+[MASTER]
+ignore = migrations
+persistent = yes
+load-plugins = edx_lint.pylint,pylint_django,pylint_celery
+
+[MESSAGES CONTROL]
+enable =
+ blacklisted-name,
+ line-too-long,
+
+ abstract-class-instantiated,
+ abstract-method,
+ access-member-before-definition,
+ anomalous-backslash-in-string,
+ anomalous-unicode-escape-in-string,
+ arguments-differ,
+ assert-on-tuple,
+ assigning-non-slot,
+ assignment-from-no-return,
+ assignment-from-none,
+ attribute-defined-outside-init,
+ bad-except-order,
+ bad-format-character,
+ bad-format-string-key,
+ bad-format-string,
+ bad-open-mode,
+ bad-reversed-sequence,
+ bad-staticmethod-argument,
+ bad-str-strip-call,
+ bad-super-call,
+ binary-op-exception,
+ boolean-datetime,
+ catching-non-exception,
+ cell-var-from-loop,
+ confusing-with-statement,
+ continue-in-finally,
+ dangerous-default-value,
+ duplicate-argument-name,
+ duplicate-bases,
+ duplicate-except,
+ duplicate-key,
+ expression-not-assigned,
+ format-combined-specification,
+ format-needs-mapping,
+ function-redefined,
+ global-variable-undefined,
+ import-error,
+ import-self,
+ inconsistent-mro,
+ inherit-non-class,
+ init-is-generator,
+ invalid-all-object,
+ invalid-format-index,
+ invalid-length-returned,
+ invalid-sequence-index,
+ invalid-slice-index,
+ invalid-slots-object,
+ invalid-slots,
+ invalid-unary-operand-type,
+ logging-too-few-args,
+ logging-too-many-args,
+ logging-unsupported-format,
+ lost-exception,
+ method-hidden,
+ misplaced-bare-raise,
+ misplaced-future,
+ missing-format-argument-key,
+ missing-format-attribute,
+ missing-format-string-key,
+ no-member,
+ no-method-argument,
+ no-name-in-module,
+ no-self-argument,
+ no-value-for-parameter,
+ non-iterator-returned,
+ non-parent-method-called,
+ nonexistent-operator,
+ not-a-mapping,
+ not-an-iterable,
+ not-callable,
+ not-context-manager,
+ not-in-loop,
+ pointless-statement,
+ pointless-string-statement,
+ raising-bad-type,
+ raising-non-exception,
+ redefined-builtin,
+ redefined-outer-name,
+ redundant-keyword-arg,
+ repeated-keyword,
+ return-arg-in-generator,
+ return-in-init,
+ return-outside-function,
+ signature-differs,
+ super-init-not-called,
+ super-method-not-called,
+ syntax-error,
+ test-inherits-tests,
+ too-few-format-args,
+ too-many-format-args,
+ too-many-function-args,
+ translation-of-non-string,
+ truncated-format-string,
+ undefined-all-variable,
+ undefined-loop-variable,
+ undefined-variable,
+ unexpected-keyword-arg,
+ unexpected-special-method-signature,
+ unpacking-non-sequence,
+ unreachable,
+ unsubscriptable-object,
+ unsupported-binary-operation,
+ unsupported-membership-test,
+ unused-format-string-argument,
+ unused-format-string-key,
+ used-before-assignment,
+ using-constant-test,
+ yield-outside-function,
+
+ astroid-error,
+ fatal,
+ method-check-failed,
+ parse-error,
+ raw-checker-failed,
+
+ empty-docstring,
+ invalid-characters-in-docstring,
+ missing-docstring,
+ wrong-spelling-in-comment,
+ wrong-spelling-in-docstring,
+
+ unused-argument,
+ unused-import,
+ unused-variable,
+
+ eval-used,
+ exec-used,
+
+ bad-classmethod-argument,
+ bad-mcs-classmethod-argument,
+ bad-mcs-method-argument,
+ bare-except,
+ broad-except,
+ consider-iterating-dictionary,
+ consider-using-enumerate,
+ global-at-module-level,
+ global-variable-not-assigned,
+ literal-used-as-attribute,
+ logging-format-interpolation,
+ logging-not-lazy,
+ multiple-imports,
+ multiple-statements,
+ no-classmethod-decorator,
+ no-staticmethod-decorator,
+ protected-access,
+ redundant-unittest-assert,
+ reimported,
+ simplifiable-if-statement,
+ simplifiable-range,
+ singleton-comparison,
+ superfluous-parens,
+ unidiomatic-typecheck,
+ unnecessary-lambda,
+ unnecessary-pass,
+ unnecessary-semicolon,
+ unneeded-not,
+ useless-else-on-loop,
+ wrong-assert-type,
+
+ deprecated-method,
+ deprecated-module,
+
+ too-many-boolean-expressions,
+ too-many-nested-blocks,
+ too-many-statements,
+
+ wildcard-import,
+ wrong-import-order,
+ wrong-import-position,
+
+ missing-final-newline,
+ mixed-line-endings,
+ trailing-newlines,
+ trailing-whitespace,
+ unexpected-line-ending-format,
+
+ bad-inline-option,
+ bad-option-value,
+ deprecated-pragma,
+ unrecognized-inline-option,
+ useless-suppression,
+disable =
+ bad-indentation,
+ broad-exception-raised,
+ consider-using-f-string,
+ duplicate-code,
+ file-ignored,
+ fixme,
+ global-statement,
+ invalid-name,
+ locally-disabled,
+ no-else-return,
+ suppressed-message,
+ too-few-public-methods,
+ too-many-ancestors,
+ too-many-arguments,
+ too-many-branches,
+ too-many-instance-attributes,
+ too-many-lines,
+ too-many-locals,
+ too-many-public-methods,
+ too-many-return-statements,
+ ungrouped-imports,
+ unspecified-encoding,
+ unused-wildcard-import,
+ use-maxsplit-arg,
+
+ feature-toggle-needs-doc,
+ illegal-waffle-usage,
+
+ logging-fstring-interpolation,
+ invalid-name,
+ django-not-configured,
+ consider-using-with,
+ bad-option-value,
+
+[REPORTS]
+output-format = text
+reports = no
+score = no
+
+[BASIC]
+module-rgx = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$
+class-rgx = [A-Z_][a-zA-Z0-9]+$
+function-rgx = ([a-z_][a-z0-9_]{2,40}|test_[a-z0-9_]+)$
+method-rgx = ([a-z_][a-z0-9_]{2,40}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$
+attr-rgx = [a-z_][a-z0-9_]{2,30}$
+argument-rgx = [a-z_][a-z0-9_]{2,30}$
+variable-rgx = [a-z_][a-z0-9_]{2,30}$
+class-attribute-rgx = ([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+inlinevar-rgx = [A-Za-z_][A-Za-z0-9_]*$
+good-names = f,i,j,k,db,ex,Run,_,__
+bad-names = foo,bar,baz,toto,tutu,tata
+no-docstring-rgx = __.*__$|test_.+|setUp$|setUpClass$|tearDown$|tearDownClass$|Meta$
+docstring-min-length = 5
+
+[FORMAT]
+max-line-length = 120
+ignore-long-lines = ^\s*(# )?((?)|(\.\. \w+: .*))$
+single-line-if-stmt = no
+max-module-lines = 1000
+indent-string = ' '
+
+[MISCELLANEOUS]
+notes = FIXME,XXX,TODO
+
+[SIMILARITIES]
+min-similarity-lines = 4
+ignore-comments = yes
+ignore-docstrings = yes
+ignore-imports = no
+
+[TYPECHECK]
+ignore-mixin-members = yes
+ignored-classes = SQLObject
+unsafe-load-any-extension = yes
+generated-members =
+ REQUEST,
+ acl_users,
+ aq_parent,
+ objects,
+ DoesNotExist,
+ can_read,
+ can_write,
+ get_url,
+ size,
+ content,
+ status_code,
+ create,
+ build,
+ fields,
+ tag,
+ org,
+ course,
+ category,
+ name,
+ revision,
+ _meta,
+
+[VARIABLES]
+init-import = no
+dummy-variables-rgx = _|dummy|unused|.*_unused
+additional-builtins =
+
+[CLASSES]
+defining-attr-methods = __init__,__new__,setUp
+valid-classmethod-first-arg = cls
+valid-metaclass-classmethod-first-arg = mcs
+
+[DESIGN]
+max-args = 5
+ignored-argument-names = _.*
+max-locals = 15
+max-returns = 6
+max-branches = 12
+max-statements = 50
+max-parents = 7
+max-attributes = 7
+min-public-methods = 2
+max-public-methods = 20
+
+[IMPORTS]
+deprecated-modules = regsub,TERMIOS,Bastion,rexec
+import-graph =
+ext-import-graph =
+int-import-graph =
+
+[EXCEPTIONS]
+overgeneral-exceptions = builtins.Exception
+
+# f9938a0048db870de9b3db6ba3d2a79f5c48d553
diff --git a/pylintrc_tweaks b/pylintrc_tweaks
new file mode 100644
index 0000000..7b6eb35
--- /dev/null
+++ b/pylintrc_tweaks
@@ -0,0 +1,11 @@
+# pylintrc tweaks for use with edx_lint.
+[MASTER]
+ignore = migrations
+load-plugins = edx_lint.pylint,pylint_django,pylint_celery
+
+[MESSAGES CONTROL]
+disable+=
+ invalid-name,
+ django-not-configured,
+ consider-using-with,
+ bad-option-value,
diff --git a/requirements/base.in b/requirements/base.in
new file mode 100644
index 0000000..a954780
--- /dev/null
+++ b/requirements/base.in
@@ -0,0 +1,5 @@
+# Core requirements for using this application
+-c constraints.txt
+
+Django # Web application framework
+
diff --git a/requirements/base.txt b/requirements/base.txt
new file mode 100644
index 0000000..a615544
--- /dev/null
+++ b/requirements/base.txt
@@ -0,0 +1,16 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# make upgrade
+#
+asgiref==3.6.0
+ # via django
+django==3.2.18
+ # via
+ # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
+ # -r requirements/base.in
+pytz==2023.3
+ # via django
+sqlparse==0.4.4
+ # via django
diff --git a/requirements/ci.in b/requirements/ci.in
new file mode 100644
index 0000000..3797849
--- /dev/null
+++ b/requirements/ci.in
@@ -0,0 +1,6 @@
+# Requirements for running tests in CI
+
+-c constraints.txt
+
+tox # Virtualenv management for tests
+tox-battery # Makes tox aware of requirements file changes
diff --git a/requirements/ci.txt b/requirements/ci.txt
new file mode 100644
index 0000000..1347919
--- /dev/null
+++ b/requirements/ci.txt
@@ -0,0 +1,33 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# make upgrade
+#
+distlib==0.3.6
+ # via virtualenv
+filelock==3.12.0
+ # via
+ # tox
+ # virtualenv
+packaging==23.1
+ # via tox
+platformdirs==3.2.0
+ # via virtualenv
+pluggy==1.0.0
+ # via tox
+py==1.11.0
+ # via tox
+six==1.16.0
+ # via tox
+tomli==2.0.1
+ # via tox
+tox==3.28.0
+ # via
+ # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
+ # -r requirements/ci.in
+ # tox-battery
+tox-battery==0.6.1
+ # via -r requirements/ci.in
+virtualenv==20.22.0
+ # via tox
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
new file mode 100644
index 0000000..d91704b
--- /dev/null
+++ b/requirements/constraints.txt
@@ -0,0 +1,12 @@
+# Version constraints for pip-installation.
+#
+# This file doesn't install any packages. It specifies version constraints
+# that will be applied if a package is needed.
+#
+# When pinning something here, please provide an explanation of why. Ideally,
+# link to other information that will help people in the future to remove the
+# pin when possible. Writing an issue against the offending project and
+# linking to it here is good.
+
+# Common constraints for edx repos
+-c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
diff --git a/requirements/dev.in b/requirements/dev.in
new file mode 100644
index 0000000..eb8c92c
--- /dev/null
+++ b/requirements/dev.in
@@ -0,0 +1,9 @@
+# Additional requirements for development of this application
+-c constraints.txt
+
+-r pip-tools.txt # pip-tools and its dependencies, for managing requirements files
+-r quality.txt # Core and quality check dependencies
+-r ci.txt # dependencies for setting up testing in CI
+
+diff-cover # Changeset diff test coverage
+edx-i18n-tools # For i18n_tool dummy
diff --git a/requirements/dev.txt b/requirements/dev.txt
new file mode 100644
index 0000000..af8de8e
--- /dev/null
+++ b/requirements/dev.txt
@@ -0,0 +1,246 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# make upgrade
+#
+asgiref==3.6.0
+ # via
+ # -r requirements/quality.txt
+ # django
+astroid==2.15.4
+ # via
+ # -r requirements/quality.txt
+ # pylint
+ # pylint-celery
+build==0.10.0
+ # via
+ # -r requirements/pip-tools.txt
+ # pip-tools
+chardet==5.1.0
+ # via diff-cover
+click==8.1.3
+ # via
+ # -r requirements/pip-tools.txt
+ # -r requirements/quality.txt
+ # click-log
+ # code-annotations
+ # edx-lint
+ # pip-tools
+click-log==0.4.0
+ # via
+ # -r requirements/quality.txt
+ # edx-lint
+code-annotations==1.3.0
+ # via
+ # -r requirements/quality.txt
+ # edx-lint
+coverage[toml]==7.2.3
+ # via
+ # -r requirements/quality.txt
+ # pytest-cov
+diff-cover==7.5.0
+ # via -r requirements/dev.in
+dill==0.3.6
+ # via
+ # -r requirements/quality.txt
+ # pylint
+distlib==0.3.6
+ # via
+ # -r requirements/ci.txt
+ # virtualenv
+django==3.2.18
+ # via
+ # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
+ # -r requirements/quality.txt
+ # edx-i18n-tools
+edx-i18n-tools==0.9.2
+ # via -r requirements/dev.in
+edx-lint==5.3.4
+ # via -r requirements/quality.txt
+exceptiongroup==1.1.1
+ # via
+ # -r requirements/quality.txt
+ # pytest
+filelock==3.12.0
+ # via
+ # -r requirements/ci.txt
+ # tox
+ # virtualenv
+iniconfig==2.0.0
+ # via
+ # -r requirements/quality.txt
+ # pytest
+isort==5.12.0
+ # via
+ # -r requirements/quality.txt
+ # pylint
+jinja2==3.1.2
+ # via
+ # -r requirements/quality.txt
+ # code-annotations
+ # diff-cover
+lazy-object-proxy==1.9.0
+ # via
+ # -r requirements/quality.txt
+ # astroid
+markupsafe==2.1.2
+ # via
+ # -r requirements/quality.txt
+ # jinja2
+mccabe==0.7.0
+ # via
+ # -r requirements/quality.txt
+ # pylint
+packaging==23.1
+ # via
+ # -r requirements/ci.txt
+ # -r requirements/pip-tools.txt
+ # -r requirements/quality.txt
+ # build
+ # pytest
+ # tox
+path==16.6.0
+ # via edx-i18n-tools
+pbr==5.11.1
+ # via
+ # -r requirements/quality.txt
+ # stevedore
+pip-tools==6.13.0
+ # via -r requirements/pip-tools.txt
+platformdirs==3.2.0
+ # via
+ # -r requirements/ci.txt
+ # -r requirements/quality.txt
+ # pylint
+ # virtualenv
+pluggy==1.0.0
+ # via
+ # -r requirements/ci.txt
+ # -r requirements/quality.txt
+ # diff-cover
+ # pytest
+ # tox
+polib==1.2.0
+ # via edx-i18n-tools
+py==1.11.0
+ # via
+ # -r requirements/ci.txt
+ # tox
+pycodestyle==2.10.0
+ # via -r requirements/quality.txt
+pydocstyle==6.3.0
+ # via -r requirements/quality.txt
+pygments==2.15.1
+ # via diff-cover
+pylint==2.17.2
+ # via
+ # -r requirements/quality.txt
+ # edx-lint
+ # pylint-celery
+ # pylint-django
+ # pylint-plugin-utils
+pylint-celery==0.3
+ # via
+ # -r requirements/quality.txt
+ # edx-lint
+pylint-django==2.5.3
+ # via
+ # -r requirements/quality.txt
+ # edx-lint
+pylint-plugin-utils==0.7
+ # via
+ # -r requirements/quality.txt
+ # pylint-celery
+ # pylint-django
+pyproject-hooks==1.0.0
+ # via
+ # -r requirements/pip-tools.txt
+ # build
+pytest==7.3.1
+ # via
+ # -r requirements/quality.txt
+ # pytest-cov
+ # pytest-django
+pytest-cov==4.0.0
+ # via -r requirements/quality.txt
+pytest-django==4.5.2
+ # via -r requirements/quality.txt
+python-slugify==8.0.1
+ # via
+ # -r requirements/quality.txt
+ # code-annotations
+pytz==2023.3
+ # via
+ # -r requirements/quality.txt
+ # django
+pyyaml==6.0
+ # via
+ # -r requirements/quality.txt
+ # code-annotations
+ # edx-i18n-tools
+six==1.16.0
+ # via
+ # -r requirements/ci.txt
+ # -r requirements/quality.txt
+ # edx-lint
+ # tox
+snowballstemmer==2.2.0
+ # via
+ # -r requirements/quality.txt
+ # pydocstyle
+sqlparse==0.4.4
+ # via
+ # -r requirements/quality.txt
+ # django
+stevedore==5.0.0
+ # via
+ # -r requirements/quality.txt
+ # code-annotations
+text-unidecode==1.3
+ # via
+ # -r requirements/quality.txt
+ # python-slugify
+tomli==2.0.1
+ # via
+ # -r requirements/ci.txt
+ # -r requirements/pip-tools.txt
+ # -r requirements/quality.txt
+ # build
+ # coverage
+ # pylint
+ # pyproject-hooks
+ # pytest
+ # tox
+tomlkit==0.11.7
+ # via
+ # -r requirements/quality.txt
+ # pylint
+tox==3.28.0
+ # via
+ # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
+ # -r requirements/ci.txt
+ # tox-battery
+tox-battery==0.6.1
+ # via -r requirements/ci.txt
+typing-extensions==4.5.0
+ # via
+ # -r requirements/quality.txt
+ # astroid
+ # pylint
+virtualenv==20.22.0
+ # via
+ # -r requirements/ci.txt
+ # tox
+wheel==0.40.0
+ # via
+ # -r requirements/pip-tools.txt
+ # pip-tools
+wrapt==1.15.0
+ # via
+ # -r requirements/quality.txt
+ # astroid
+
+# The following packages are considered to be unsafe in a requirements file:
+# pip
+# setuptools
diff --git a/requirements/doc.in b/requirements/doc.in
new file mode 100644
index 0000000..a8b0290
--- /dev/null
+++ b/requirements/doc.in
@@ -0,0 +1,10 @@
+# Requirements for documentation validation
+-c constraints.txt
+
+-r test.txt # Core and testing dependencies for this package
+
+doc8 # reStructuredText style checker
+sphinx-book-theme # Common theme for all Open edX projects
+twine # Validates README.rst for usage on PyPI
+build # Needed to build the wheel for twine check
+Sphinx # Documentation builder
diff --git a/requirements/doc.txt b/requirements/doc.txt
new file mode 100644
index 0000000..5caa34f
--- /dev/null
+++ b/requirements/doc.txt
@@ -0,0 +1,218 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# make upgrade
+#
+accessible-pygments==0.0.4
+ # via pydata-sphinx-theme
+alabaster==0.7.13
+ # via sphinx
+asgiref==3.6.0
+ # via
+ # -r requirements/test.txt
+ # django
+babel==2.12.1
+ # via
+ # pydata-sphinx-theme
+ # sphinx
+beautifulsoup4==4.12.2
+ # via pydata-sphinx-theme
+bleach==6.0.0
+ # via readme-renderer
+build==0.10.0
+ # via -r requirements/doc.in
+certifi==2022.12.7
+ # via requests
+charset-normalizer==3.1.0
+ # via requests
+click==8.1.3
+ # via
+ # -r requirements/test.txt
+ # code-annotations
+code-annotations==1.3.0
+ # via -r requirements/test.txt
+coverage[toml]==7.2.3
+ # via
+ # -r requirements/test.txt
+ # pytest-cov
+django==3.2.18
+ # via
+ # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
+ # -r requirements/test.txt
+doc8==1.1.1
+ # via -r requirements/doc.in
+docutils==0.19
+ # via
+ # doc8
+ # pydata-sphinx-theme
+ # readme-renderer
+ # restructuredtext-lint
+ # sphinx
+exceptiongroup==1.1.1
+ # via
+ # -r requirements/test.txt
+ # pytest
+idna==3.4
+ # via requests
+imagesize==1.4.1
+ # via sphinx
+importlib-metadata==6.6.0
+ # via
+ # keyring
+ # sphinx
+ # twine
+importlib-resources==5.12.0
+ # via keyring
+iniconfig==2.0.0
+ # via
+ # -r requirements/test.txt
+ # pytest
+jaraco-classes==3.2.3
+ # via keyring
+jinja2==3.1.2
+ # via
+ # -r requirements/test.txt
+ # code-annotations
+ # sphinx
+keyring==23.13.1
+ # via twine
+markdown-it-py==2.2.0
+ # via rich
+markupsafe==2.1.2
+ # via
+ # -r requirements/test.txt
+ # jinja2
+mdurl==0.1.2
+ # via markdown-it-py
+more-itertools==9.1.0
+ # via jaraco-classes
+packaging==23.1
+ # via
+ # -r requirements/test.txt
+ # build
+ # pydata-sphinx-theme
+ # pytest
+ # sphinx
+pbr==5.11.1
+ # via
+ # -r requirements/test.txt
+ # stevedore
+pkginfo==1.9.6
+ # via twine
+pluggy==1.0.0
+ # via
+ # -r requirements/test.txt
+ # pytest
+pydata-sphinx-theme==0.13.3
+ # via sphinx-book-theme
+pygments==2.15.1
+ # via
+ # accessible-pygments
+ # doc8
+ # pydata-sphinx-theme
+ # readme-renderer
+ # rich
+ # sphinx
+pyproject-hooks==1.0.0
+ # via build
+pytest==7.3.1
+ # via
+ # -r requirements/test.txt
+ # pytest-cov
+ # pytest-django
+pytest-cov==4.0.0
+ # via -r requirements/test.txt
+pytest-django==4.5.2
+ # via -r requirements/test.txt
+python-slugify==8.0.1
+ # via
+ # -r requirements/test.txt
+ # code-annotations
+pytz==2023.3
+ # via
+ # -r requirements/test.txt
+ # babel
+ # django
+pyyaml==6.0
+ # via
+ # -r requirements/test.txt
+ # code-annotations
+readme-renderer==37.3
+ # via twine
+requests==2.28.2
+ # via
+ # requests-toolbelt
+ # sphinx
+ # twine
+requests-toolbelt==0.10.1
+ # via twine
+restructuredtext-lint==1.4.0
+ # via doc8
+rfc3986==2.0.0
+ # via twine
+rich==13.3.4
+ # via twine
+six==1.16.0
+ # via bleach
+snowballstemmer==2.2.0
+ # via sphinx
+soupsieve==2.4.1
+ # via beautifulsoup4
+sphinx==5.3.0
+ # via
+ # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
+ # -r requirements/doc.in
+ # pydata-sphinx-theme
+ # sphinx-book-theme
+sphinx-book-theme==1.0.1
+ # via -r requirements/doc.in
+sphinxcontrib-applehelp==1.0.4
+ # via sphinx
+sphinxcontrib-devhelp==1.0.2
+ # via sphinx
+sphinxcontrib-htmlhelp==2.0.1
+ # via sphinx
+sphinxcontrib-jsmath==1.0.1
+ # via sphinx
+sphinxcontrib-qthelp==1.0.3
+ # via sphinx
+sphinxcontrib-serializinghtml==1.1.5
+ # via sphinx
+sqlparse==0.4.4
+ # via
+ # -r requirements/test.txt
+ # django
+stevedore==5.0.0
+ # via
+ # -r requirements/test.txt
+ # code-annotations
+ # doc8
+text-unidecode==1.3
+ # via
+ # -r requirements/test.txt
+ # python-slugify
+tomli==2.0.1
+ # via
+ # -r requirements/test.txt
+ # build
+ # coverage
+ # doc8
+ # pyproject-hooks
+ # pytest
+twine==4.0.2
+ # via -r requirements/doc.in
+typing-extensions==4.5.0
+ # via
+ # pydata-sphinx-theme
+ # rich
+urllib3==1.26.15
+ # via
+ # requests
+ # twine
+webencodings==0.5.1
+ # via bleach
+zipp==3.15.0
+ # via
+ # importlib-metadata
+ # importlib-resources
diff --git a/requirements/pip-tools.in b/requirements/pip-tools.in
new file mode 100644
index 0000000..0295d2c
--- /dev/null
+++ b/requirements/pip-tools.in
@@ -0,0 +1,5 @@
+# Just the dependencies to run pip-tools, mainly for the "upgrade" make target
+
+-c constraints.txt
+
+pip-tools # Contains pip-compile, used to generate pip requirements files
diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt
new file mode 100644
index 0000000..fd0cc1c
--- /dev/null
+++ b/requirements/pip-tools.txt
@@ -0,0 +1,24 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# make upgrade
+#
+build==0.10.0
+ # via pip-tools
+click==8.1.3
+ # via pip-tools
+packaging==23.1
+ # via build
+pip-tools==6.13.0
+ # via -r requirements/pip-tools.in
+pyproject-hooks==1.0.0
+ # via build
+tomli==2.0.1
+ # via build
+wheel==0.40.0
+ # via pip-tools
+
+# The following packages are considered to be unsafe in a requirements file:
+# pip
+# setuptools
diff --git a/requirements/pip.in b/requirements/pip.in
new file mode 100644
index 0000000..716c6f2
--- /dev/null
+++ b/requirements/pip.in
@@ -0,0 +1,6 @@
+# Core dependencies for installing other packages
+-c constraints.txt
+
+pip
+setuptools
+wheel
diff --git a/requirements/pip.txt b/requirements/pip.txt
new file mode 100644
index 0000000..4767414
--- /dev/null
+++ b/requirements/pip.txt
@@ -0,0 +1,14 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# make upgrade
+#
+wheel==0.40.0
+ # via -r requirements/pip.in
+
+# The following packages are considered to be unsafe in a requirements file:
+pip==23.1.1
+ # via -r requirements/pip.in
+setuptools==67.7.2
+ # via -r requirements/pip.in
diff --git a/requirements/private.readme b/requirements/private.readme
new file mode 100644
index 0000000..5600a10
--- /dev/null
+++ b/requirements/private.readme
@@ -0,0 +1,15 @@
+# If there are any Python packages you want to keep in your virtualenv beyond
+# those listed in the official requirements files, create a "private.in" file
+# and list them there. Generate the corresponding "private.txt" file pinning
+# all of their indirect dependencies to specific versions as follows:
+
+# pip-compile private.in
+
+# This allows you to use "pip-sync" without removing these packages:
+
+# pip-sync requirements/*.txt
+
+# "private.in" and "private.txt" aren't checked into git to avoid merge
+# conflicts, and the presence of this file allows "private.*" to be
+# included in scripted pip-sync usage without requiring that those files be
+# created first.
diff --git a/requirements/quality.in b/requirements/quality.in
new file mode 100644
index 0000000..93661d9
--- /dev/null
+++ b/requirements/quality.in
@@ -0,0 +1,10 @@
+# Requirements for code quality checks
+
+-c constraints.txt
+
+-r test.txt # Core and testing dependencies for this package
+
+edx-lint # edX pylint rules and plugins
+isort # to standardize order of imports
+pycodestyle # PEP 8 compliance validation
+pydocstyle # PEP 257 compliance validation
diff --git a/requirements/quality.txt b/requirements/quality.txt
new file mode 100644
index 0000000..1e3309c
--- /dev/null
+++ b/requirements/quality.txt
@@ -0,0 +1,145 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# make upgrade
+#
+asgiref==3.6.0
+ # via
+ # -r requirements/test.txt
+ # django
+astroid==2.15.4
+ # via
+ # pylint
+ # pylint-celery
+click==8.1.3
+ # via
+ # -r requirements/test.txt
+ # click-log
+ # code-annotations
+ # edx-lint
+click-log==0.4.0
+ # via edx-lint
+code-annotations==1.3.0
+ # via
+ # -r requirements/test.txt
+ # edx-lint
+coverage[toml]==7.2.3
+ # via
+ # -r requirements/test.txt
+ # pytest-cov
+dill==0.3.6
+ # via pylint
+django==3.2.18
+ # via
+ # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
+ # -r requirements/test.txt
+edx-lint==5.3.4
+ # via -r requirements/quality.in
+exceptiongroup==1.1.1
+ # via
+ # -r requirements/test.txt
+ # pytest
+iniconfig==2.0.0
+ # via
+ # -r requirements/test.txt
+ # pytest
+isort==5.12.0
+ # via
+ # -r requirements/quality.in
+ # pylint
+jinja2==3.1.2
+ # via
+ # -r requirements/test.txt
+ # code-annotations
+lazy-object-proxy==1.9.0
+ # via astroid
+markupsafe==2.1.2
+ # via
+ # -r requirements/test.txt
+ # jinja2
+mccabe==0.7.0
+ # via pylint
+packaging==23.1
+ # via
+ # -r requirements/test.txt
+ # pytest
+pbr==5.11.1
+ # via
+ # -r requirements/test.txt
+ # stevedore
+platformdirs==3.2.0
+ # via pylint
+pluggy==1.0.0
+ # via
+ # -r requirements/test.txt
+ # pytest
+pycodestyle==2.10.0
+ # via -r requirements/quality.in
+pydocstyle==6.3.0
+ # via -r requirements/quality.in
+pylint==2.17.2
+ # via
+ # edx-lint
+ # pylint-celery
+ # pylint-django
+ # pylint-plugin-utils
+pylint-celery==0.3
+ # via edx-lint
+pylint-django==2.5.3
+ # via edx-lint
+pylint-plugin-utils==0.7
+ # via
+ # pylint-celery
+ # pylint-django
+pytest==7.3.1
+ # via
+ # -r requirements/test.txt
+ # pytest-cov
+ # pytest-django
+pytest-cov==4.0.0
+ # via -r requirements/test.txt
+pytest-django==4.5.2
+ # via -r requirements/test.txt
+python-slugify==8.0.1
+ # via
+ # -r requirements/test.txt
+ # code-annotations
+pytz==2023.3
+ # via
+ # -r requirements/test.txt
+ # django
+pyyaml==6.0
+ # via
+ # -r requirements/test.txt
+ # code-annotations
+six==1.16.0
+ # via edx-lint
+snowballstemmer==2.2.0
+ # via pydocstyle
+sqlparse==0.4.4
+ # via
+ # -r requirements/test.txt
+ # django
+stevedore==5.0.0
+ # via
+ # -r requirements/test.txt
+ # code-annotations
+text-unidecode==1.3
+ # via
+ # -r requirements/test.txt
+ # python-slugify
+tomli==2.0.1
+ # via
+ # -r requirements/test.txt
+ # coverage
+ # pylint
+ # pytest
+tomlkit==0.11.7
+ # via pylint
+typing-extensions==4.5.0
+ # via
+ # astroid
+ # pylint
+wrapt==1.15.0
+ # via astroid
diff --git a/requirements/test.in b/requirements/test.in
new file mode 100644
index 0000000..6797160
--- /dev/null
+++ b/requirements/test.in
@@ -0,0 +1,8 @@
+# Requirements for test runs.
+-c constraints.txt
+
+-r base.txt # Core dependencies for this package
+
+pytest-cov # pytest extension for code coverage statistics
+pytest-django # pytest extension for better Django support
+code-annotations # provides commands used by the pii_check make target.
diff --git a/requirements/test.txt b/requirements/test.txt
new file mode 100644
index 0000000..4dbf8d8
--- /dev/null
+++ b/requirements/test.txt
@@ -0,0 +1,61 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# make upgrade
+#
+asgiref==3.6.0
+ # via
+ # -r requirements/base.txt
+ # django
+click==8.1.3
+ # via code-annotations
+code-annotations==1.3.0
+ # via -r requirements/test.in
+coverage[toml]==7.2.3
+ # via pytest-cov
+ # via
+ # -c https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt
+ # -r requirements/base.txt
+exceptiongroup==1.1.1
+ # via pytest
+iniconfig==2.0.0
+ # via pytest
+jinja2==3.1.2
+ # via code-annotations
+markupsafe==2.1.2
+ # via jinja2
+packaging==23.1
+ # via pytest
+pbr==5.11.1
+ # via stevedore
+pluggy==1.0.0
+ # via pytest
+pytest==7.3.1
+ # via
+ # pytest-cov
+ # pytest-django
+pytest-cov==4.0.0
+ # via -r requirements/test.in
+pytest-django==4.5.2
+ # via -r requirements/test.in
+python-slugify==8.0.1
+ # via code-annotations
+pytz==2023.3
+ # via
+ # -r requirements/base.txt
+ # django
+pyyaml==6.0
+ # via code-annotations
+sqlparse==0.4.4
+ # via
+ # -r requirements/base.txt
+ # django
+stevedore==5.0.0
+ # via code-annotations
+text-unidecode==1.3
+ # via python-slugify
+tomli==2.0.1
+ # via
+ # coverage
+ # pytest
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..d782599
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,10 @@
+[isort]
+include_trailing_comma = True
+indent = ' '
+line_length = 120
+multi_line_output = 3
+skip=
+ migrations
+
+[wheel]
+universal = 1
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..ea1feb7
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+"""
+Package metadata for event_sink_clickhouse.
+"""
+import os
+import re
+import sys
+
+from setuptools import find_packages, setup
+
+
+def get_version(*file_paths):
+ """
+ Extract the version string from the file.
+
+ Input:
+ - file_paths: relative path fragments to file with
+ version string
+ """
+ filename = os.path.join(os.path.dirname(__file__), *file_paths)
+ version_file = open(filename, encoding="utf8").read()
+ version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
+ version_file, re.M)
+ if version_match:
+ return version_match.group(1)
+ raise RuntimeError('Unable to find version string.')
+
+
+def load_requirements(*requirements_paths):
+ """
+ Load all requirements from the specified requirements files.
+
+ Requirements will include any constraints from files specified
+ with -c in the requirements files.
+ Returns a list of requirement strings.
+ """
+ requirements = {}
+ constraint_files = set()
+
+ # groups "pkg<=x.y.z,..." into ("pkg", "<=x.y.z,...")
+ requirement_line_regex = re.compile(r"([a-zA-Z0-9-_.]+)([<>=][^#\s]+)?")
+
+ def add_version_constraint_or_raise(current_line, current_requirements, add_if_not_present):
+ regex_match = requirement_line_regex.match(current_line)
+ if regex_match:
+ package = regex_match.group(1)
+ version_constraints = regex_match.group(2)
+ existing_version_constraints = current_requirements.get(package, None)
+ # fine to add constraints to an unconstrained package,
+ # raise an error if there are already constraints in place
+ if existing_version_constraints and existing_version_constraints != version_constraints:
+ raise BaseException(f'Multiple constraint definitions found for {package}:'
+ f' "{existing_version_constraints}" and "{version_constraints}".'
+ f'Combine constraints into one location with {package}'
+ f'{existing_version_constraints},{version_constraints}.')
+ if add_if_not_present or package in current_requirements:
+ current_requirements[package] = version_constraints
+
+ # read requirements from .in
+ # store the path to any constraint files that are pulled in
+ for path in requirements_paths:
+ with open(path) as reqs:
+ for line in reqs:
+ if is_requirement(line):
+ add_version_constraint_or_raise(line, requirements, True)
+ if line and line.startswith('-c') and not line.startswith('-c http'):
+ constraint_files.add(os.path.dirname(path) + '/' + line.split('#')[0].replace('-c', '').strip())
+
+ # process constraint files: add constraints to existing requirements
+ for constraint_file in constraint_files:
+ with open(constraint_file) as reader:
+ for line in reader:
+ if is_requirement(line):
+ add_version_constraint_or_raise(line, requirements, False)
+
+ # process back into list of pkg><=constraints strings
+ constrained_requirements = [f'{pkg}{version or ""}' for (pkg, version) in sorted(requirements.items())]
+ return constrained_requirements
+
+
+def is_requirement(line):
+ """
+ Return True if the requirement line is a package requirement.
+
+ Returns:
+ bool: True if the line is not blank, a comment,
+ a URL, or an included file
+ """
+ return line and line.strip() and not line.startswith(("-r", "#", "-e", "git+", "-c"))
+
+
+VERSION = get_version('event_sink_clickhouse', '__init__.py')
+
+if sys.argv[-1] == 'tag':
+ print("Tagging the version on github:")
+ os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION))
+ os.system("git push --tags")
+ sys.exit()
+
+README = open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding="utf8").read()
+CHANGELOG = open(os.path.join(os.path.dirname(__file__), 'CHANGELOG.rst'), encoding="utf8").read()
+
+setup(
+ name='openedx_event_sink_clickhouse',
+ version=VERSION,
+ description="""A sink for Open edX events to send them to ClickHouse""",
+ long_description=README + '\n\n' + CHANGELOG,
+ author='edX',
+ author_email='oscm@edx.org',
+ url='https://github.com/openedx/openedx_event_sink_clickhouse',
+ packages=find_packages(
+ include=['event_sink_clickhouse', 'event_sink_clickhouse.*'],
+ exclude=["*tests"],
+ ),
+
+ include_package_data=True,
+ install_requires=load_requirements('requirements/base.in'),
+ python_requires=">=3.8",
+ license="AGPL 3.0",
+ zip_safe=False,
+ keywords='Python edx',
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ 'Framework :: Django',
+ 'Framework :: Django :: 3.2',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
+ 'Natural Language :: English',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.8',
+ ],
+)
diff --git a/test_settings.py b/test_settings.py
new file mode 100644
index 0000000..58f2d14
--- /dev/null
+++ b/test_settings.py
@@ -0,0 +1,61 @@
+"""
+These settings are here to use during tests, because django requires them.
+
+In a real-world use case, apps in this project are installed into other
+Django applications, so these settings will not be used.
+"""
+
+from os.path import abspath, dirname, join
+
+
+def root(*args):
+ """
+ Get the absolute path of the given path relative to the project root.
+ """
+ return join(abspath(dirname(__file__)), *args)
+
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'default.db',
+ 'USER': '',
+ 'PASSWORD': '',
+ 'HOST': '',
+ 'PORT': '',
+ }
+}
+
+INSTALLED_APPS = (
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.messages',
+ 'django.contrib.sessions',
+ 'event_sink_clickhouse',
+)
+
+LOCALE_PATHS = [
+ root('event_sink_clickhouse', 'conf', 'locale'),
+]
+
+ROOT_URLCONF = 'event_sink_clickhouse.urls'
+
+SECRET_KEY = 'insecure-secret-key'
+
+MIDDLEWARE = (
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+)
+
+TEMPLATES = [{
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'APP_DIRS': False,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.contrib.auth.context_processors.auth', # this is required for admin
+ 'django.contrib.messages.context_processors.messages', # this is required for admin
+ ],
+ },
+}]
diff --git a/test_utils/__init__.py b/test_utils/__init__.py
new file mode 100644
index 0000000..7961e47
--- /dev/null
+++ b/test_utils/__init__.py
@@ -0,0 +1,10 @@
+"""
+Test utilities.
+
+Since pytest discourages putting __init__.py into testdirectory
+(i.e. making tests a package) one cannot import from anywhere
+under tests folder. However, some utility classes/methods might be useful
+in multiple test modules (i.e. factoryboy factories, base test classes).
+
+So this package is the place to put them.
+"""
diff --git a/tests/test_models.py b/tests/test_models.py
new file mode 100644
index 0000000..7477d0a
--- /dev/null
+++ b/tests/test_models.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+"""
+Tests for the `openedx_event_sink_clickhouse` models module.
+"""
+
+
+def test_placeholder():
+ """
+ Placeholder to allow pytest to succeed before real tests are in place.
+
+ (If there are no tests, it will exit with code 5.)
+ """
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..9b0e194
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,89 @@
+[tox]
+envlist = py38-django{32,40}
+
+[doc8]
+; D001 = Line too long
+ignore = D001
+
+[pycodestyle]
+exclude = .git,.tox,migrations
+max-line-length = 120
+
+[pydocstyle]
+; D101 = Missing docstring in public class
+; D200 = One-line docstring should fit on one line with quotes
+; D203 = 1 blank line required before class docstring
+; D212 = Multi-line docstring summary should start at the first line
+; D215 = Section underline is over-indented (numpy style)
+; D404 = First word of the docstring should not be This (numpy style)
+; D405 = Section name should be properly capitalized (numpy style)
+; D406 = Section name should end with a newline (numpy style)
+; D407 = Missing dashed underline after section (numpy style)
+; D408 = Section underline should be in the line following the section's name (numpy style)
+; D409 = Section underline should match the length of its name (numpy style)
+; D410 = Missing blank line after section (numpy style)
+; D411 = Missing blank line before section (numpy style)
+; D412 = No blank lines allowed between a section header and its content (numpy style)
+; D413 = Missing blank line after last section (numpy style)
+; D414 = Section has no content (numpy style)
+ignore = D101,D200,D203,D212,D215,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414
+match-dir = (?!migrations)
+
+[pytest]
+DJANGO_SETTINGS_MODULE = test_settings
+addopts = --cov event_sink_clickhouse --cov-report term-missing --cov-report xml
+norecursedirs = .* docs requirements site-packages
+
+[testenv]
+deps =
+ django32: Django>=3.2,<4.0
+ django40: Django>=4.0,<4.1
+ -r{toxinidir}/requirements/test.txt
+commands =
+ python manage.py check
+ pytest {posargs}
+
+[testenv:docs]
+setenv =
+ DJANGO_SETTINGS_MODULE = test_settings
+ PYTHONPATH = {toxinidir}
+ # Adding the option here instead of as a default in the docs Makefile because that Makefile is generated by shpinx.
+ SPHINXOPTS = -W
+whitelist_externals =
+ make
+ rm
+deps =
+ -r{toxinidir}/requirements/doc.txt
+commands =
+ doc8 --ignore-path docs/_build README.rst docs
+ rm -f docs/event_sink_clickhouse.rst
+ rm -f docs/modules.rst
+ make -e -C docs clean
+ make -e -C docs html
+ python -m build --wheel
+ twine check dist/*
+
+[testenv:quality]
+whitelist_externals =
+ make
+ rm
+ touch
+deps =
+ -r{toxinidir}/requirements/quality.txt
+commands =
+ touch tests/__init__.py
+ pylint event_sink_clickhouse tests test_utils manage.py setup.py
+ rm tests/__init__.py
+ pycodestyle event_sink_clickhouse tests manage.py setup.py
+ pydocstyle event_sink_clickhouse tests manage.py setup.py
+ isort --check-only --diff tests test_utils event_sink_clickhouse manage.py setup.py test_settings.py
+ make selfcheck
+
+[testenv:pii_check]
+setenv =
+ DJANGO_SETTINGS_MODULE = test_settings
+deps =
+ -r{toxinidir}/requirements/test.txt
+commands =
+ code_annotations django_find_annotations --config_file .pii_annotations.yml --lint --report --coverage
+