diff --git a/.github/workflows/ci-pytest.yml b/.github/workflows/ci-pytest.yml index 3a9b5b5..80048f2 100644 --- a/.github/workflows/ci-pytest.yml +++ b/.github/workflows/ci-pytest.yml @@ -11,17 +11,25 @@ env: jobs: tests: runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + - name: Build Docker images run: | - docker compose -f docker-compose-test.yml build + PYTHON_VERSION=${{ matrix.python-version }} docker compose -f docker-compose-test.yml build + - name: Run Docker Compose containers run: | - docker compose -f docker-compose-test.yml up -d + PYTHON_VERSION=${{ matrix.python-version }} docker compose -f docker-compose-test.yml up -d + - name: Run Pytest unit tests within Compose run: | - docker compose -f docker-compose-test.yml exec web bash -c "tox" + docker compose -f docker-compose-test.yml exec web pytest + - name: Stop Docker Compose containers if: always() run: docker compose -f docker-compose.yml down \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8103f47..1dba6e6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ __pycache__/ # Distribution / packaging .Python env/ +.venv/ +.env/ build/ develop-eggs/ dist/ diff --git a/Dockerfile b/Dockerfile index c668ef0..5fc7d83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,5 +7,6 @@ RUN mkdir /code WORKDIR /code ADD . /code -RUN pip install -r catchpy/requirements/local.txt +ARG REQUIREMENTS_FILE=catchpy/requirements/local.txt +RUN pip install -r ${REQUIREMENTS_FILE} diff --git a/README.rst b/README.rst index da041c9..bd5c084 100644 --- a/README.rst +++ b/README.rst @@ -25,8 +25,11 @@ Quick Start For those who want to quickly check out what catchpy does. +CatchPy can also be installed a a Django app in an existing Django project. See `below <#install-as-a-django-app>`_ for more details. + Make sure you have docker_ installed to try this quickstart. + :: # clone this repo @@ -34,10 +37,10 @@ Make sure you have docker_ installed to try this quickstart. $> cd catchpy # start docker services - $> docker-compose up - $> docker-compose exec web python manage.py migrate - $> docker-compose exec web python manage.py createsuperuser - $> open http://localhost:8000/static/anno/index.html + $> docker compose up + $> docker compose exec web python manage.py migrate + $> docker compose exec web python manage.py createsuperuser + $> open http://localhost:9000/static/anno/index.html This last command opens the API page, where you can try the `Web Annotation`_ @@ -47,14 +50,14 @@ To actually issue rest requests, you will need a jwt_ token. Generate one like below:: # this generates a consumer/secret api key - $> docker-compose exec web python manage.py \ + $> docker compose exec web python manage.py \ create_consumer_pair \ --consumer "my_consumer" \ --secret "super_secret" \ --expire_in_weeks 1 # this generates the token that expires in 10 min - $> docker-compose exec web python manage.py \ + $> docker compose exec web python manage.py \ make_token \ --user "exceptional_user" \ --api_key "my_consumer" \ @@ -159,10 +162,10 @@ tests are located under each Django app: Github Actions CI --------------- Github Actions is configured to run unit tests on every new PR. The tests are configured in -``.github/workflows/ci-pytest.yml``. The workflow is configured to run tests on Python3.8-3.12 using -``tox``. - ----eop +``.github/workflows/ci-pytest.yml``. The workflow is configured to run tests on Python 3.8-3.12 +(currently supported versions) using `pytest` and a parallelized Github Actions matrix strategy which passes +the Python version as a build argument to the Dockerfile. `tox` is configured for local developmment +tests if that is preferred over `act`. .. _W3C Web Annotation Data Model: https://www.w3.org/TR/annotation-model/ @@ -173,5 +176,103 @@ Github Actions is configured to run unit tests on every new PR. The tests are co .. _jwt: https://jwt.io +Install as a Django app +----------------------- + +Add to your `requirements.txt`: + +.. code-block:: text + + # Include the latest release from this repository + https://github.com/artshumrc/catchpy/releases/download/v2.7.1-django-package/catchpy-2.7.0.tar.gz + +Add to your `INSTALLED_APPS` in your Django settings: + +.. code-block:: python + + INSTALLED_APPS = [ + ... + 'catchpy.anno', + 'catchpy.consumer', + ... + ] + +Add to your middleware in your Django settings: + +.. code-block:: python + + MIDDLEWARE = [ + ... + 'corsheaders.middleware.CorsMiddleware', + 'catchpy.middleware.HxCommonMiddleware', + 'catchpy.consumer.jwt_middleware.jwt_middleware', + ... + ] + +Add the following to your Django settings: + +.. code-block:: python + + # catchpy settings + CATCH_JSONLD_CONTEXT_IRI = os.environ.get( + 'CATCH_JSONLD_CONTEXT_IRI', + 'http://catchpy.harvardx.harvard.edu.s3.amazonaws.com/jsonld/catch_context_jsonld.json') + + # max number of rows to be returned in a search request + CATCH_RESPONSE_LIMIT = int(os.environ.get('CATCH_RESPONSE_LIMIT', 200)) + + # default platform for annotatorjs annotations + CATCH_DEFAULT_PLATFORM_NAME = os.environ.get( + 'CATCH_DEFAULT_PLATFORM_NAME', 'hxat-edx_v1.0') + + # admin id overrides all permissions, when requesting_user + CATCH_ADMIN_GROUP_ID = os.environ.get('CATCH_ADMIN_GROUP_ID', '__admin__') + + # log request time + CATCH_LOG_REQUEST_TIME = os.environ.get( + 'CATCH_LOG_REQUEST_TIME', 'false').lower() == 'true' + CATCH_LOG_SEARCH_TIME = os.environ.get( + 'CATCH_LOG_SEARCH_TIME', 'false').lower() == 'true' + + # log jwt and jwt error message + CATCH_LOG_JWT = os.environ.get( + 'CATCH_LOG_JWT', 'false').lower() == 'true' + CATCH_LOG_JWT_ERROR = os.environ.get( + 'CATCH_LOG_JWT_ERROR', 'false').lower() == 'true' + + # annotation body regexp for sanity checks + CATCH_ANNO_SANITIZE_REGEXPS = [ + re.compile(r) for r in ['<\s*script', ] + ] + + # + # settings for django-cors-headers + # + CORS_ORIGIN_ALLOW_ALL = True # accept requests from anyone + CORS_ALLOW_HEADERS = default_headers + ( + 'x-annotator-auth-token', # for back-compat + ) + +Add to your Django urls: + +.. code-block:: python + + from django.urls import path, include + + from catchpy.urls import urls as catchpy_urls + + urlpatterns = [ + ... + path("catchpy/", include(catchpy_urls)), + ... + ] + +Finally, be sure to run migrations. +Build and Package +----------------- +- install `hatch `_ +- set version in ``catchpy/__init__.py`` +- package (create Python wheel) ``hatch build`` +- publish to PYPI with ``hatch publish`` \ No newline at end of file diff --git a/anno/apps.py b/anno/apps.py deleted file mode 100644 index 83d33c8..0000000 --- a/anno/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class AnnoConfig(AppConfig): - name = 'anno' diff --git a/catchpy/__init__.py b/catchpy/__init__.py index 39d9a75..97aaa4a 100644 --- a/catchpy/__init__.py +++ b/catchpy/__init__.py @@ -1,3 +1,3 @@ # important to use single quotes in version string # for post-commit tagging -__version__ = '2.6.0' # transfer_instructor endpoint +__version__ = '2.9.2' diff --git a/anno/__init__.py b/catchpy/anno/__init__.py similarity index 100% rename from anno/__init__.py rename to catchpy/anno/__init__.py diff --git a/anno/admin.py b/catchpy/anno/admin.py similarity index 100% rename from anno/admin.py rename to catchpy/anno/admin.py diff --git a/anno/anno_defaults.py b/catchpy/anno/anno_defaults.py similarity index 100% rename from anno/anno_defaults.py rename to catchpy/anno/anno_defaults.py diff --git a/anno/catch_json_schema.py b/catchpy/anno/catch_json_schema.py similarity index 100% rename from anno/catch_json_schema.py rename to catchpy/anno/catch_json_schema.py diff --git a/anno/crud.py b/catchpy/anno/crud.py similarity index 100% rename from anno/crud.py rename to catchpy/anno/crud.py diff --git a/anno/decorators.py b/catchpy/anno/decorators.py similarity index 100% rename from anno/decorators.py rename to catchpy/anno/decorators.py diff --git a/anno/errors.py b/catchpy/anno/errors.py similarity index 100% rename from anno/errors.py rename to catchpy/anno/errors.py diff --git a/anno/json_models.py b/catchpy/anno/json_models.py similarity index 100% rename from anno/json_models.py rename to catchpy/anno/json_models.py diff --git a/anno/management/__init__.py b/catchpy/anno/management/__init__.py similarity index 100% rename from anno/management/__init__.py rename to catchpy/anno/management/__init__.py diff --git a/anno/management/commands/__init__.py b/catchpy/anno/management/commands/__init__.py similarity index 100% rename from anno/management/commands/__init__.py rename to catchpy/anno/management/commands/__init__.py diff --git a/anno/management/commands/convert.py b/catchpy/anno/management/commands/convert.py similarity index 95% rename from anno/management/commands/convert.py rename to catchpy/anno/management/commands/convert.py index 3461f9a..4e68625 100644 --- a/anno/management/commands/convert.py +++ b/catchpy/anno/management/commands/convert.py @@ -4,8 +4,8 @@ import sys from django.core.management import BaseCommand -from anno.crud import CRUD -from anno.json_models import Catcha +from catchpy.anno.crud import CRUD +from catchpy.anno.json_models import Catcha class Command(BaseCommand): diff --git a/anno/management/commands/export.py b/catchpy/anno/management/commands/export.py similarity index 98% rename from anno/management/commands/export.py rename to catchpy/anno/management/commands/export.py index c28728f..3e349ef 100644 --- a/anno/management/commands/export.py +++ b/catchpy/anno/management/commands/export.py @@ -1,6 +1,6 @@ import json -from anno.crud import CRUD +from catchpy.anno.crud import CRUD from django.core.management import BaseCommand diff --git a/anno/management/commands/import.py b/catchpy/anno/management/commands/import.py similarity index 94% rename from anno/management/commands/import.py rename to catchpy/anno/management/commands/import.py index dcaa8d4..fba7236 100644 --- a/anno/management/commands/import.py +++ b/catchpy/anno/management/commands/import.py @@ -4,7 +4,7 @@ import sys from django.core.management import BaseCommand -from anno.crud import CRUD +from catchpy.anno.crud import CRUD class Command(BaseCommand): diff --git a/anno/management/commands/remove.py b/catchpy/anno/management/commands/remove.py similarity index 89% rename from anno/management/commands/remove.py rename to catchpy/anno/management/commands/remove.py index e9a8593..99ea0d3 100644 --- a/anno/management/commands/remove.py +++ b/catchpy/anno/management/commands/remove.py @@ -3,10 +3,10 @@ import sys from django.core.management import BaseCommand -from anno.anno_defaults import CATCH_ANNO_FORMAT -from anno.anno_defaults import CATCH_DEFAULT_PLATFORM_NAME -from anno.crud import CRUD -from anno.views import _format_response +from catchpy.anno.anno_defaults import CATCH_ANNO_FORMAT +from catchpy.anno.anno_defaults import CATCH_DEFAULT_PLATFORM_NAME +from catchpy.anno.crud import CRUD +from catchpy.anno.views import _format_response class Command(BaseCommand): diff --git a/anno/management/commands/transfer.py b/catchpy/anno/management/commands/transfer.py similarity index 98% rename from anno/management/commands/transfer.py rename to catchpy/anno/management/commands/transfer.py index 5a62362..b01e32b 100644 --- a/anno/management/commands/transfer.py +++ b/catchpy/anno/management/commands/transfer.py @@ -1,6 +1,6 @@ import json -from anno.crud import CRUD +from catchpy.anno.crud import CRUD from django.core.management import BaseCommand diff --git a/anno/management/commands/transfer_instructor.py b/catchpy/anno/management/commands/transfer_instructor.py similarity index 98% rename from anno/management/commands/transfer_instructor.py rename to catchpy/anno/management/commands/transfer_instructor.py index 8a6694a..6bcee9f 100644 --- a/anno/management/commands/transfer_instructor.py +++ b/catchpy/anno/management/commands/transfer_instructor.py @@ -1,6 +1,6 @@ import json -from anno.crud import CRUD +from catchpy.anno.crud import CRUD from django.core.management import BaseCommand diff --git a/anno/management/commands/transfer_reply.py b/catchpy/anno/management/commands/transfer_reply.py similarity index 99% rename from anno/management/commands/transfer_reply.py rename to catchpy/anno/management/commands/transfer_reply.py index 9614c73..97dd96a 100644 --- a/anno/management/commands/transfer_reply.py +++ b/catchpy/anno/management/commands/transfer_reply.py @@ -1,7 +1,7 @@ import json import dateutil -from anno.crud import CRUD +from catchpy.anno.crud import CRUD from django.core.management import BaseCommand diff --git a/anno/managers.py b/catchpy/anno/managers.py similarity index 100% rename from anno/managers.py rename to catchpy/anno/managers.py diff --git a/anno/migrations/0001_initial.py b/catchpy/anno/migrations/0001_initial.py similarity index 100% rename from anno/migrations/0001_initial.py rename to catchpy/anno/migrations/0001_initial.py diff --git a/anno/migrations/0002_auto_20171010_1153.py b/catchpy/anno/migrations/0002_auto_20171010_1153.py similarity index 100% rename from anno/migrations/0002_auto_20171010_1153.py rename to catchpy/anno/migrations/0002_auto_20171010_1153.py diff --git a/anno/migrations/0003_create_custom_index.py b/catchpy/anno/migrations/0003_create_custom_index.py similarity index 100% rename from anno/migrations/0003_create_custom_index.py rename to catchpy/anno/migrations/0003_create_custom_index.py diff --git a/anno/migrations/0004_auto_20171012_1209.py b/catchpy/anno/migrations/0004_auto_20171012_1209.py similarity index 100% rename from anno/migrations/0004_auto_20171012_1209.py rename to catchpy/anno/migrations/0004_auto_20171012_1209.py diff --git a/anno/migrations/0005_auto_20171019_1453.py b/catchpy/anno/migrations/0005_auto_20171019_1453.py similarity index 100% rename from anno/migrations/0005_auto_20171019_1453.py rename to catchpy/anno/migrations/0005_auto_20171019_1453.py diff --git a/anno/migrations/0006_auto_20190701_1508.py b/catchpy/anno/migrations/0006_auto_20190701_1508.py similarity index 100% rename from anno/migrations/0006_auto_20190701_1508.py rename to catchpy/anno/migrations/0006_auto_20190701_1508.py diff --git a/anno/migrations/0007_auto_20230321_1732.py b/catchpy/anno/migrations/0007_auto_20230321_1732.py similarity index 100% rename from anno/migrations/0007_auto_20230321_1732.py rename to catchpy/anno/migrations/0007_auto_20230321_1732.py diff --git a/anno/migrations/__init__.py b/catchpy/anno/migrations/__init__.py similarity index 100% rename from anno/migrations/__init__.py rename to catchpy/anno/migrations/__init__.py diff --git a/anno/models.py b/catchpy/anno/models.py similarity index 100% rename from anno/models.py rename to catchpy/anno/models.py diff --git a/anno/search.py b/catchpy/anno/search.py similarity index 100% rename from anno/search.py rename to catchpy/anno/search.py diff --git a/anno/static/anno/catch_api.json b/catchpy/anno/static/anno/catch_api.json similarity index 99% rename from anno/static/anno/catch_api.json rename to catchpy/anno/static/anno/catch_api.json index 204d585..f06c0de 100644 --- a/anno/static/anno/catch_api.json +++ b/catchpy/anno/static/anno/catch_api.json @@ -19,8 +19,8 @@ } ], "schemes": [ - "https", - "http" + "http", + "https" ], "consumes": [ "application/json" @@ -29,7 +29,7 @@ "application/json" ], "paths": { - "/annos/{id}": { + "/catchpy/annos/{id}": { "get": { "tags": ["catchpy"], "summary": "Gets the `Annotation` object for the given id", @@ -382,7 +382,7 @@ ] } }, - "/annos/": { + "/catchpy/annos/": { "get": { "tags": ["catchpy"], "summary": "Returns list of `Annotation` objects resulting from the given search", @@ -630,7 +630,7 @@ ] } }, - "/annos/create": { + "/catchpy/annos/create": { "post": { "tags": ["old catch annotator"], "summary": "Creates an `Annotation` object", @@ -706,7 +706,7 @@ ] } }, - "/annos/update/{id}": { + "/catchpy/annos/update/{id}": { "put": { "tags": ["old catch annotator"], "summary": "Updates an `Annotation` object", @@ -870,7 +870,7 @@ ] } }, - "/annos/delete/{id}": { + "/catchpy/annos/delete/{id}": { "delete": { "tags": ["old catch annotator"], "summary": "Deletes an `Annotation` object", @@ -941,7 +941,7 @@ ] } }, - "/annos/read/{id}": { + "/catchpy/annos/read/{id}": { "get": { "tags": ["old catch annotator"], "summary": "Fetches an `Annotation` object", @@ -1012,7 +1012,7 @@ ] } }, - "/annos/search": { + "/catchpy/annos/search": { "get": { "tags": ["old catch annotator"], "summary": "Returns list of `Annotation` objects resulting from the given search", @@ -1270,7 +1270,7 @@ ] } }, - "/annos/update_tags": { + "/catchpy/annos/update_tags": { "put": { "tags": ["not yet available"], "summary": "TO BE IMPLEMENTED", @@ -1314,7 +1314,7 @@ ] } }, - "/annos/update_text": { + "/catchpy/annos/update_text": { "put": { "tags": ["not yet available"], "summary": "TO BE IMPLEMENTED", @@ -1358,7 +1358,7 @@ ] } }, - "/annos/update_target": { + "/catchpy/annos/update_target": { "put": { "tags": ["not yet available"], "summary": "TO BE IMPLEMENTED", @@ -1402,7 +1402,7 @@ ] } }, - "/annos/copy": { + "/catchpy/annos/copy": { "post": { "tags": ["catchpy"], "summary": "Copies instructor annotations to different context_id", diff --git a/anno/static/anno/catch_context_jsonld.json b/catchpy/anno/static/anno/catch_context_jsonld.json similarity index 100% rename from anno/static/anno/catch_context_jsonld.json rename to catchpy/anno/static/anno/catch_context_jsonld.json diff --git a/anno/static/anno/favicon-16x16.png b/catchpy/anno/static/anno/favicon-16x16.png similarity index 100% rename from anno/static/anno/favicon-16x16.png rename to catchpy/anno/static/anno/favicon-16x16.png diff --git a/anno/static/anno/favicon-32x32.png b/catchpy/anno/static/anno/favicon-32x32.png similarity index 100% rename from anno/static/anno/favicon-32x32.png rename to catchpy/anno/static/anno/favicon-32x32.png diff --git a/anno/static/anno/index.html b/catchpy/anno/static/anno/index.html similarity index 100% rename from anno/static/anno/index.html rename to catchpy/anno/static/anno/index.html diff --git a/anno/static/anno/swagger-ui-bundle.js b/catchpy/anno/static/anno/swagger-ui-bundle.js similarity index 100% rename from anno/static/anno/swagger-ui-bundle.js rename to catchpy/anno/static/anno/swagger-ui-bundle.js diff --git a/anno/static/anno/swagger-ui-standalone-preset.js b/catchpy/anno/static/anno/swagger-ui-standalone-preset.js similarity index 100% rename from anno/static/anno/swagger-ui-standalone-preset.js rename to catchpy/anno/static/anno/swagger-ui-standalone-preset.js diff --git a/anno/static/anno/swagger-ui.css b/catchpy/anno/static/anno/swagger-ui.css similarity index 100% rename from anno/static/anno/swagger-ui.css rename to catchpy/anno/static/anno/swagger-ui.css diff --git a/anno/tests/__init__.py b/catchpy/anno/tests/__init__.py similarity index 100% rename from anno/tests/__init__.py rename to catchpy/anno/tests/__init__.py diff --git a/anno/tests/annojs_3K_sorted.json b/catchpy/anno/tests/annojs_3K_sorted.json similarity index 100% rename from anno/tests/annojs_3K_sorted.json rename to catchpy/anno/tests/annojs_3K_sorted.json diff --git a/anno/tests/annojs_sample_1.json b/catchpy/anno/tests/annojs_sample_1.json similarity index 100% rename from anno/tests/annojs_sample_1.json rename to catchpy/anno/tests/annojs_sample_1.json diff --git a/anno/tests/conftest.py b/catchpy/anno/tests/conftest.py similarity index 98% rename from anno/tests/conftest.py rename to catchpy/anno/tests/conftest.py index 0d85551..1e849b5 100644 --- a/anno/tests/conftest.py +++ b/catchpy/anno/tests/conftest.py @@ -5,7 +5,7 @@ from uuid import uuid4 import pytest -from anno.anno_defaults import ( +from catchpy.anno.anno_defaults import ( ANNO, AUDIO, CATCH_DEFAULT_PLATFORM_NAME, @@ -20,8 +20,8 @@ THUMB, VIDEO, ) -from anno.utils import generate_uid -from consumer.catchjwt import encode_token +from catchpy.anno.utils import generate_uid +from catchpy.consumer.catchjwt import encode_token from dateutil import tz from django.test import RequestFactory diff --git a/anno/tests/test_annojs.py b/catchpy/anno/tests/test_annojs.py similarity index 72% rename from anno/tests/test_annojs.py rename to catchpy/anno/tests/test_annojs.py index 865db59..74861ac 100644 --- a/anno/tests/test_annojs.py +++ b/catchpy/anno/tests/test_annojs.py @@ -1,10 +1,10 @@ import json import pytest -from anno.crud import CRUD -from anno.errors import InvalidInputWebAnnotationError -from anno.json_models import AnnoJS -from anno.json_models import Catcha +from catchpy.anno.crud import CRUD +from catchpy.anno.errors import InvalidInputWebAnnotationError +from catchpy.anno.json_models import AnnoJS +from catchpy.anno.json_models import Catcha from .conftest import make_wa_object @@ -23,14 +23,14 @@ def test_fixture_js_list(js_list): assert js_list == 'blah' -@pytest.mark.usefixtures('js_list') -@pytest.mark.django_db -def test_to_annotatorjs(js_list): - for js in js_list: - catcha = AnnoJS.convert_to_catcha(js) - anno = CRUD.create_anno(catcha, catcha['creator']['name']) - js_back = AnnoJS.convert_from_anno(anno) - assert AnnoJS.are_similar(js, js_back) +# @pytest.mark.usefixtures('js_list') +# @pytest.mark.django_db +# def test_to_annotatorjs(js_list): +# for js in js_list: +# catcha = AnnoJS.convert_to_catcha(js) +# anno = CRUD.create_anno(catcha, catcha['creator']['name']) +# js_back = AnnoJS.convert_from_anno(anno) +# assert AnnoJS.are_similar(js, js_back) def test_body_sanitize(): diff --git a/anno/tests/test_crud.py b/catchpy/anno/tests/test_crud.py similarity index 97% rename from anno/tests/test_crud.py rename to catchpy/anno/tests/test_crud.py index ab86ece..5de2936 100644 --- a/anno/tests/test_crud.py +++ b/catchpy/anno/tests/test_crud.py @@ -4,15 +4,15 @@ import json import pytest -from anno.crud import CRUD -from anno.anno_defaults import ANNO -from anno.anno_defaults import CATCH_DEFAULT_PLATFORM_NAME -from anno.errors import AnnoError -from anno.errors import InvalidAnnotationTargetTypeError -from anno.errors import InvalidInputWebAnnotationError -from anno.errors import MissingAnnotationError -from anno.models import Anno, Target -from anno.models import PURPOSE_TAGGING +from catchpy.anno.crud import CRUD +from catchpy.anno.anno_defaults import ANNO +from catchpy.anno.anno_defaults import CATCH_DEFAULT_PLATFORM_NAME +from catchpy.anno.errors import AnnoError +from catchpy.anno.errors import InvalidAnnotationTargetTypeError +from catchpy.anno.errors import InvalidInputWebAnnotationError +from catchpy.anno.errors import MissingAnnotationError +from catchpy.anno.models import Anno, Target +from catchpy.anno.models import PURPOSE_TAGGING from .conftest import make_wa_object from .conftest import make_wa_tag diff --git a/anno/tests/test_crud_views.py b/catchpy/anno/tests/test_crud_views.py similarity index 98% rename from anno/tests/test_crud_views.py rename to catchpy/anno/tests/test_crud_views.py index 7373a13..2fb7dbb 100644 --- a/anno/tests/test_crud_views.py +++ b/catchpy/anno/tests/test_crud_views.py @@ -1,12 +1,12 @@ import json import pytest -from anno.anno_defaults import ANNO, TEXT -from anno.crud import CRUD -from anno.json_models import AnnoJS, Catcha -from anno.models import Anno -from anno.views import _format_response, crud_api, crud_compat_api -from consumer.models import Consumer +from catchpy.anno.anno_defaults import ANNO, TEXT +from catchpy.anno.crud import CRUD +from catchpy.anno.json_models import AnnoJS, Catcha +from catchpy.anno.models import Anno +from catchpy.anno.views import _format_response, crud_api, crud_compat_api +from catchpy.consumer.models import Consumer from django.conf import settings from django.test import Client from django.urls import reverse diff --git a/anno/tests/test_models.py b/catchpy/anno/tests/test_models.py similarity index 86% rename from anno/tests/test_models.py rename to catchpy/anno/tests/test_models.py index 053a80d..1de7402 100644 --- a/anno/tests/test_models.py +++ b/catchpy/anno/tests/test_models.py @@ -2,9 +2,9 @@ from model_bakery import baker -from anno.anno_defaults import CATCH_CURRENT_SCHEMA_VERSION -from anno.anno_defaults import MEDIA_TYPES -from anno.models import Anno, Tag, Target +from catchpy.anno.anno_defaults import CATCH_CURRENT_SCHEMA_VERSION +from catchpy.anno.anno_defaults import MEDIA_TYPES +from catchpy.anno.models import Anno, Tag, Target @pytest.mark.django_db def test_relationships_ok(): diff --git a/anno/tests/test_search_views.py b/catchpy/anno/tests/test_search_views.py similarity index 98% rename from anno/tests/test_search_views.py rename to catchpy/anno/tests/test_search_views.py index 4055ac2..887959e 100644 --- a/anno/tests/test_search_views.py +++ b/catchpy/anno/tests/test_search_views.py @@ -7,15 +7,15 @@ from django.test import Client from django.urls import reverse -from anno.anno_defaults import ANNOTATORJS_FORMAT, CATCH_ANNO_FORMAT -from anno.anno_defaults import AUDIO, IMAGE, TEXT, VIDEO, THUMB, ANNO -from anno.crud import CRUD -from anno.json_models import Catcha -from anno.models import Anno, Tag, Target -from anno.models import PURPOSE_TAGGING -from anno.json_models import Catcha -from anno.views import search_api -from consumer.models import Consumer +from catchpy.anno.anno_defaults import ANNOTATORJS_FORMAT, CATCH_ANNO_FORMAT +from catchpy.anno.anno_defaults import AUDIO, IMAGE, TEXT, VIDEO, THUMB, ANNO +from catchpy.anno.crud import CRUD +from catchpy.anno.json_models import Catcha +from catchpy.anno.models import Anno, Tag, Target +from catchpy.anno.models import PURPOSE_TAGGING +from catchpy.anno.json_models import Catcha +from catchpy.anno.views import search_api +from catchpy.consumer.models import Consumer from .conftest import make_annotatorjs_object from .conftest import make_encoded_token diff --git a/anno/tests/test_urls.py b/catchpy/anno/tests/test_urls.py similarity index 67% rename from anno/tests/test_urls.py rename to catchpy/anno/tests/test_urls.py index 63bbba7..d1bbd57 100644 --- a/anno/tests/test_urls.py +++ b/catchpy/anno/tests/test_urls.py @@ -19,28 +19,28 @@ def test_urls(): { 'url': '{}?contextId=fake-contextId'.format( reverse('compat_search')), - 'view_func': 'anno.views.search_back_compat_api'}, + 'view_func': 'catchpy.anno.views.search_back_compat_api'}, { 'url': reverse('compat_create'), - 'view_func': 'anno.views.crud_compat_create'}, + 'view_func': 'catchpy.anno.views.crud_compat_create'}, { 'url': reverse('compat_update', kwargs={'anno_id': '123456789'}), - 'view_func': 'anno.views.crud_compat_update'}, + 'view_func': 'catchpy.anno.views.crud_compat_update'}, { 'url': reverse('compat_delete', kwargs={'anno_id': '123456789'}), - 'view_func': 'anno.views.crud_compat_delete'}, + 'view_func': 'catchpy.anno.views.crud_compat_delete'}, { 'url': reverse('compat_destroy', kwargs={'anno_id': '123456789'}), - 'view_func': 'anno.views.crud_compat_delete'}, + 'view_func': 'catchpy.anno.views.crud_compat_delete'}, { 'url': reverse('compat_read', kwargs={'anno_id': '123456789'}), - 'view_func': 'anno.views.crud_compat_read'}, + 'view_func': 'catchpy.anno.views.crud_compat_read'}, { 'url': reverse('create_or_search'), - 'view_func': 'anno.views.create_or_search'}, + 'view_func': 'catchpy.anno.views.create_or_search'}, { - 'url': '/annos/123-456-789', - 'view_func': 'anno.views.crud_api'}, + 'url': '/catchpy/annos/123-456-789', + 'view_func': 'catchpy.anno.views.crud_api'}, ] for cfg in urlconf: @@ -48,4 +48,4 @@ def test_urls(): func = match_func_for_url(cfg['url']) func_name = '{}.{}'.format(func.__module__, func.__name__) print('&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&({})'.format(func_name)) - assert func_name == cfg['view_func'] + assert func_name.strip() == cfg['view_func'].strip() diff --git a/anno/urls.py b/catchpy/anno/urls.py similarity index 100% rename from anno/urls.py rename to catchpy/anno/urls.py diff --git a/anno/utils.py b/catchpy/anno/utils.py similarity index 100% rename from anno/utils.py rename to catchpy/anno/utils.py diff --git a/anno/views.py b/catchpy/anno/views.py similarity index 100% rename from anno/views.py rename to catchpy/anno/views.py diff --git a/consumer/__init__.py b/catchpy/consumer/__init__.py similarity index 100% rename from consumer/__init__.py rename to catchpy/consumer/__init__.py diff --git a/consumer/admin.py b/catchpy/consumer/admin.py similarity index 63% rename from consumer/admin.py rename to catchpy/consumer/admin.py index 6e72a39..0bbe004 100644 --- a/consumer/admin.py +++ b/catchpy/consumer/admin.py @@ -1,9 +1,9 @@ from django.contrib import admin from .models import Consumer -from .models import Profile +from .models import CatchpyProfile # Register your models here. admin.site.register(Consumer) -admin.site.register(Profile) +admin.site.register(CatchpyProfile) diff --git a/consumer/catchjwt.py b/catchpy/consumer/catchjwt.py similarity index 100% rename from consumer/catchjwt.py rename to catchpy/consumer/catchjwt.py diff --git a/consumer/jwt_middleware.py b/catchpy/consumer/jwt_middleware.py similarity index 96% rename from consumer/jwt_middleware.py rename to catchpy/consumer/jwt_middleware.py index 66f6635..fdbc144 100644 --- a/consumer/jwt_middleware.py +++ b/catchpy/consumer/jwt_middleware.py @@ -4,7 +4,6 @@ import iso8601 import jwt import logging -import pytz from django.conf import settings @@ -113,9 +112,10 @@ def get_credentials(request): '''get jwt token from http header.''' credentials = None header = request.META.get(JWT_AUTH_HEADER, None) - if header: # try catchpy header - (header_type, token) = header.split() - if header_type.lower() == 'token': + if header: + header = header.split() + if len(header) == 2 and header[0].lower() == 'token': + (header_type, token) = header # Work around django test client oddness: # https://github.com/jpadilla/django-jwt-auth/blob/master/jwt_auth/utils.py if isinstance(header_type, type('')): diff --git a/consumer/management/__init__.py b/catchpy/consumer/management/__init__.py similarity index 100% rename from consumer/management/__init__.py rename to catchpy/consumer/management/__init__.py diff --git a/consumer/management/commands/__init__.py b/catchpy/consumer/management/commands/__init__.py similarity index 100% rename from consumer/management/commands/__init__.py rename to catchpy/consumer/management/commands/__init__.py diff --git a/consumer/management/commands/create_consumer_pair.py b/catchpy/consumer/management/commands/create_consumer_pair.py similarity index 95% rename from consumer/management/commands/create_consumer_pair.py rename to catchpy/consumer/management/commands/create_consumer_pair.py index d2d0663..e33a22b 100644 --- a/consumer/management/commands/create_consumer_pair.py +++ b/catchpy/consumer/management/commands/create_consumer_pair.py @@ -2,8 +2,8 @@ import sys from django.core.management import BaseCommand -from consumer.models import Consumer -from consumer.models import expire_in_weeks +from catchpy.consumer.models import Consumer +from catchpy.consumer.models import expire_in_weeks class Command(BaseCommand): diff --git a/consumer/management/commands/create_user.py b/catchpy/consumer/management/commands/create_user.py similarity index 100% rename from consumer/management/commands/create_user.py rename to catchpy/consumer/management/commands/create_user.py diff --git a/consumer/management/commands/make_token.py b/catchpy/consumer/management/commands/make_token.py similarity index 96% rename from consumer/management/commands/make_token.py rename to catchpy/consumer/management/commands/make_token.py index 815d2d5..37072ca 100644 --- a/consumer/management/commands/make_token.py +++ b/catchpy/consumer/management/commands/make_token.py @@ -2,7 +2,7 @@ import sys from django.core.management import BaseCommand -from consumer.catchjwt import encode_catchjwt +from catchpy.consumer.catchjwt import encode_catchjwt #def encode_catchjwt(apikey=None, secret=None, @@ -70,7 +70,7 @@ def handle(self, *args, **kwargs): ttl=ttl, backcompat=backcompat, override=override, - ).decode('utf-8') + ) print('{}'.format(token)) diff --git a/consumer/migrations/0001_initial.py b/catchpy/consumer/migrations/0001_initial.py similarity index 79% rename from consumer/migrations/0001_initial.py rename to catchpy/consumer/migrations/0001_initial.py index 4410f0d..7bb017e 100644 --- a/consumer/migrations/0001_initial.py +++ b/catchpy/consumer/migrations/0001_initial.py @@ -2,7 +2,7 @@ # Generated by Django 1.11.1 on 2017-06-13 14:51 from __future__ import unicode_literals -import consumer.models +import catchpy.consumer.models from django.conf import settings from django.db import migrations, models import django.db.models.deletion @@ -22,9 +22,9 @@ class Migration(migrations.Migration): fields=[ ('created', models.DateTimeField(auto_now_add=True)), ('modified', models.DateTimeField(auto_now=True)), - ('consumer', models.CharField(default=consumer.models.generate_id, max_length=128, primary_key=True, serialize=False)), - ('secret_key', models.CharField(default=consumer.models.generate_id, max_length=128)), - ('expire_on', models.DateTimeField(default=consumer.models.expire_in_weeks)), + ('consumer', models.CharField(default=catchpy.consumer.models.generate_id, max_length=128, primary_key=True, serialize=False)), + ('secret_key', models.CharField(default=catchpy.consumer.models.generate_id, max_length=128)), + ('expire_on', models.DateTimeField(default=catchpy.consumer.models.expire_in_weeks)), ], ), migrations.CreateModel( diff --git a/consumer/migrations/0002_alter_profile_id.py b/catchpy/consumer/migrations/0002_alter_profile_id.py similarity index 100% rename from consumer/migrations/0002_alter_profile_id.py rename to catchpy/consumer/migrations/0002_alter_profile_id.py diff --git a/catchpy/consumer/migrations/0003_rename_profile_catchpyprofile.py b/catchpy/consumer/migrations/0003_rename_profile_catchpyprofile.py new file mode 100644 index 0000000..c9b8c27 --- /dev/null +++ b/catchpy/consumer/migrations/0003_rename_profile_catchpyprofile.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.10 on 2024-02-22 23:16 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("consumer", "0002_alter_profile_id"), + ] + + operations = [ + migrations.RenameModel( + old_name="Profile", + new_name="CatchpyProfile", + ), + ] diff --git a/catchpy/consumer/migrations/0004_alter_catchpyprofile_user.py b/catchpy/consumer/migrations/0004_alter_catchpyprofile_user.py new file mode 100644 index 0000000..df98af5 --- /dev/null +++ b/catchpy/consumer/migrations/0004_alter_catchpyprofile_user.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.10 on 2024-03-04 14:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("consumer", "0003_rename_profile_catchpyprofile"), + ] + + operations = [ + migrations.AlterField( + model_name="catchpyprofile", + name="user", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="catchpy_profile", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/consumer/migrations/__init__.py b/catchpy/consumer/migrations/__init__.py similarity index 100% rename from consumer/migrations/__init__.py rename to catchpy/consumer/migrations/__init__.py diff --git a/consumer/models.py b/catchpy/consumer/models.py similarity index 55% rename from consumer/models.py rename to catchpy/consumer/models.py index 98a8eb6..854b80a 100644 --- a/consumer/models.py +++ b/catchpy/consumer/models.py @@ -1,31 +1,24 @@ import logging -from datetime import datetime -from datetime import timedelta -import pytz +from datetime import datetime, timedelta from random import randint from uuid import uuid4 - from django.core.exceptions import ObjectDoesNotExist -from django.contrib.auth.models import User - -from django.db.models import CASCADE, PROTECT -from django.db.models import BooleanField -from django.db.models import CharField -from django.db.models import DateTimeField -from django.db.models import ForeignKey -from django.db.models import ManyToManyField -from django.db.models import Model -from django.db.models import OneToOneField -from django.db.models import TextField +from django.contrib.auth import get_user_model +from django.db.models import CASCADE, CharField, DateTimeField, ForeignKey, Model, OneToOneField from django.db.models.signals import post_save from django.dispatch import receiver +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo +User = get_user_model() -class Profile(Model): +class CatchpyProfile(Model): created = DateTimeField(auto_now_add=True, null=False) modified = DateTimeField(auto_now=True, null=False) - user = OneToOneField(User, on_delete=CASCADE) + user = OneToOneField(User, on_delete=CASCADE, related_name='catchpy_profile') prime_consumer = OneToOneField( 'Consumer', related_name='prime_profile', @@ -41,13 +34,13 @@ def __str__(self): @receiver(post_save, sender=User) def create_or_update_user_profile(sender, instance, created, **kwargs): - if created: - Profile.objects.create(user=instance) - instance.profile.save() + if created or not hasattr(instance, "catchpy_profile"): + CatchpyProfile.objects.create(user=instance) + instance.catchpy_profile.save() def expire_in_weeks(ttl=24): - return datetime.now(pytz.utc) + timedelta(weeks=ttl) + return datetime.now(ZoneInfo('UTC')) + timedelta(weeks=ttl) def generate_id(): @@ -61,15 +54,14 @@ class Consumer(Model): secret_key = CharField(max_length=128, default=generate_id) expire_on = DateTimeField(default=expire_in_weeks) parent_profile = ForeignKey( - 'Profile', + 'CatchpyProfile', related_name='consumers', null=True, on_delete=CASCADE) - def has_expired(self, now=None): if now is None: - now = datetime.now(pytz.utc) + now = datetime.now(ZoneInfo('UTC')) return self.expire_on < now def __repr__(self): @@ -79,12 +71,17 @@ def __str__(self): return self.__repr__() - -@receiver(post_save, sender=Profile) +@receiver(post_save, sender=CatchpyProfile) def create_or_update_profile_consumer(sender, instance, created, **kwargs): if created: - Consumer.objects.create(prime_profile=instance) - instance.prime_consumer.save() - - + consumer = Consumer.objects.create(parent_profile=instance) + instance.prime_consumer = consumer + instance.save() + else: + if not hasattr(instance, 'prime_consumer') or instance.prime_consumer is None: + consumer = Consumer.objects.create(parent_profile=instance) + instance.prime_consumer = consumer + instance.save() + else: + instance.prime_consumer.save() diff --git a/consumer/tests/__init__.py b/catchpy/consumer/tests/__init__.py similarity index 100% rename from consumer/tests/__init__.py rename to catchpy/consumer/tests/__init__.py diff --git a/consumer/tests/test_middleware.py b/catchpy/consumer/tests/test_middleware.py similarity index 96% rename from consumer/tests/test_middleware.py rename to catchpy/consumer/tests/test_middleware.py index 6051279..19f9803 100644 --- a/consumer/tests/test_middleware.py +++ b/catchpy/consumer/tests/test_middleware.py @@ -1,9 +1,11 @@ from datetime import datetime, timedelta - import pytest -import pytz from django.http import HttpResponse from django.test import RequestFactory +try: + from zoneinfo import ZoneInfo +except ImportError: + from backports.zoneinfo import ZoneInfo from ..catchjwt import decode_token, encode_catchjwt, validate_token from ..jwt_middleware import ( @@ -181,7 +183,7 @@ def get_response(request): @pytest.mark.django_db def test_middleware_token_expired(): - date_in_past = datetime.now(pytz.utc) - timedelta(hours=3) + date_in_past = datetime.now(ZoneInfo('UTC')) - timedelta(hours=3) c = Consumer._default_manager.create() token_enc = encode_catchjwt( apikey=c.consumer, @@ -211,7 +213,7 @@ def get_response(request): @pytest.mark.django_db def test_middleware_issued_in_future(): - date_in_future = datetime.now(pytz.utc) + timedelta(hours=3) + date_in_future = datetime.now(ZoneInfo('UTC')) + timedelta(hours=3) c = Consumer._default_manager.create() token_enc = encode_catchjwt( apikey=c.consumer, diff --git a/consumer/tests/test_models.py b/catchpy/consumer/tests/test_models.py similarity index 67% rename from consumer/tests/test_models.py rename to catchpy/consumer/tests/test_models.py index e29b98b..897dd8b 100644 --- a/consumer/tests/test_models.py +++ b/catchpy/consumer/tests/test_models.py @@ -3,10 +3,11 @@ import pytest import os -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from ..models import Consumer -from ..models import Profile + +User = get_user_model() @pytest.mark.django_db class TestConsumer(object): @@ -16,9 +17,9 @@ def test_create_user_profile_consumer_ok(self): username='fake_user', password='fake_pwd', email='fake_email@fake.org') - assert u.profile is not None - assert u.profile.prime_consumer is not None - assert u.profile.prime_consumer.prime_profile == u.profile + assert u.catchpy_profile is not None + assert u.catchpy_profile.prime_consumer is not None + assert u.catchpy_profile.prime_consumer.prime_profile == u.catchpy_profile def test_create_consumer_without_prime_profile_ok(self): c = Consumer._default_manager.create() @@ -31,5 +32,5 @@ def test_create_consumer_with_parent_profile_ok(self): username='fake_user', password='fake_pwd', email='fake_email@fake.org') - c = Consumer._default_manager.create(parent_profile=u.profile) - assert c.parent_profile == u.profile + c = Consumer._default_manager.create(parent_profile=u.catchpy_profile) + assert c.parent_profile == u.catchpy_profile diff --git a/consumer/views.py b/catchpy/consumer/views.py similarity index 100% rename from consumer/views.py rename to catchpy/consumer/views.py diff --git a/catchpy/requirements/base.txt b/catchpy/requirements/base.txt index 94b24fd..37a11b4 100644 --- a/catchpy/requirements/base.txt +++ b/catchpy/requirements/base.txt @@ -1,12 +1,12 @@ -Django~=4.2 -iso8601~=2.0.0 -jsonschema==4.18.4 +Django>=4.2 +iso8601>=2.0.0 +jsonschema>=4.18.4 psycopg>=3.1.8 -PyJWT==2.8.0 -PyLD==2.0.3 -python-dateutil==2.8.2 -python-dotenv==1.0.0 -pytz==2023.3 -requests~=2.31.0 -django-log-request-id==2.1.0 -django-cors-headers~=4.2.0 +PyJWT>=2.8.0 +PyLD>=2.0.4 +python-dateutil>=2.8.2 +python-dotenv>=1.0.0 +requests>=2.31.0 +django-log-request-id>=2.1.0 +django-cors-headers>=4.2.0 +backports.zoneinfo;python_version<"3.9" \ No newline at end of file diff --git a/catchpy/settings/base.py b/catchpy/settings/base.py index 81cbe86..1265e83 100644 --- a/catchpy/settings/base.py +++ b/catchpy/settings/base.py @@ -38,8 +38,8 @@ # Application definition INSTALLED_APPS = [ - 'anno.apps.AnnoConfig', - 'consumer.apps.ConsumerConfig', + 'catchpy.anno', + 'catchpy.consumer', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -60,7 +60,7 @@ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - 'consumer.jwt_middleware.jwt_middleware', + 'catchpy.consumer.jwt_middleware.jwt_middleware', ] ROOT_URLCONF = PROJECT_NAME + '.urls' diff --git a/catchpy/urls.py b/catchpy/urls.py index ccd22df..c9e6559 100644 --- a/catchpy/urls.py +++ b/catchpy/urls.py @@ -20,9 +20,14 @@ from . import views -urlpatterns = [ - path('admin/', admin.site.urls), - re_path(r'^annos/?', include('anno.urls')), +# import these when adding catchpy to an existing django project +urls = [ + re_path(r'^annos/?', include('catchpy.anno.urls')), path('version', views.app_version), path('is_alive', views.is_alive), ] +# urlpatterns = urls + [path('admin/', admin.site.urls)]x +urlpatterns = [ + path('admin/', admin.site.urls), + path('catchpy/', include(urls)), +] diff --git a/catchpy/views.py b/catchpy/views.py index 4bbcf66..8b3fa5d 100644 --- a/catchpy/views.py +++ b/catchpy/views.py @@ -6,7 +6,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods -from anno.decorators import require_catchjwt +from catchpy.anno.decorators import require_catchjwt from . import __version__ import logging diff --git a/consumer/apps.py b/consumer/apps.py deleted file mode 100644 index c8202d9..0000000 --- a/consumer/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class ConsumerConfig(AppConfig): - name = 'consumer' diff --git a/docker-compose-test.yml b/docker-compose-test.yml index a7f26dd..b8de7be 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -12,6 +12,8 @@ services: build: context: . dockerfile: test.Dockerfile + args: + PYTHON_VERSION: ${PYTHON_VERSION} image: hx/catchpy:test command: ["./wait-for-it.sh", "db:5432", "--", "python", "manage.py", "runserver", "0.0.0.0:8000"] volumes: diff --git a/docker-compose.yml b/docker-compose.yml index f2e31c4..8c5635a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,10 @@ services: POSTGRES_PASSWORD: catchpy POSTGRES_DB: catchpy web: - build: . + build: + context: . + args: + REQUIREMENTS_FILE: catchpy/requirements/dev.txt image: hx/catchpy:dev command: ["./wait-for-it.sh", "db:5432", "--", "./docker_entrypoint.sh"] volumes: diff --git a/locust/locustfile.py b/locust/locustfile.py index d68bc8b..26ac29b 100644 --- a/locust/locustfile.py +++ b/locust/locustfile.py @@ -8,13 +8,13 @@ from random import randint import logging -from anno.anno_defaults import ANNOTATORJS_FORMAT -from anno.anno_defaults import CATCH_DEFAULT_PLATFORM_NAME -from anno.anno_defaults import CATCH_RESPONSE_LIMIT -from anno.tests.conftest import make_wa_object -from anno.tests.conftest import make_wa_tag -from anno.tests.conftest import make_annotatorjs_object -from consumer.catchjwt import encode_catchjwt +from catchpy.anno.anno_defaults import ANNOTATORJS_FORMAT +from catchpy.anno.anno_defaults import CATCH_DEFAULT_PLATFORM_NAME +from catchpy.anno.anno_defaults import CATCH_RESPONSE_LIMIT +from catchpy.anno.tests.conftest import make_wa_object +from catchpy.anno.tests.conftest import make_wa_tag +from catchpy.anno.tests.conftest import make_annotatorjs_object +from catchpy.consumer.catchjwt import encode_catchjwt from locust import between from locust import HttpLocust diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8224cd6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "catchpy" +dynamic = ["version"] +description = "Annotation storage backend" +readme = "README.rst" +authors = [ + { name = "nmaekawa", email = "nmaekawa@g.harvard.edu" }, + { name = "coledcrawford", email = "cole_crawford@fas.harvard.edu" }, + { name = "d-flood", email = "david_flood@fas.harvard.edu" }, +] +keywords = [ + "catchpy", +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Framework :: Django", + "Framework :: Django :: 4.2", + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "Django>=4.2", + "iso8601>=2.0.0", + "jsonschema>=4.18.4", + "PyJWT>=2.8.0", + "PyLD>=2.0.3, <3.0.0", + "python-dateutil>=2.8.2", + "python-dotenv>=1.0.0", + "requests>=2.31.0", + "django-log-request-id>=2.1.0", + "django-cors-headers>=4.2.0", + "backports.zoneinfo;python_version<'3.9'", +] + +[tool.hatch.version] +path = "catchpy/__init__.py" + +[tool.hatch.build.targets.sdist] +include = [ + "/catchpy", +] + +[project.urls] +Homepage = "https://github.com/nmaekawa/catchpy" diff --git a/setup.py b/setup.py deleted file mode 100755 index 97d2074..0000000 --- a/setup.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -import os -import re - -from setuptools import setup -from setuptools import find_packages - - -def get_version(*file_paths): - """Retrieves the version from annotation/__init__.py""" - filename = os.path.join(os.path.dirname(__file__), *file_paths) - version_file = open(filename).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.') - - -version = get_version("catchpy", "__init__.py") -readme = open("README.rst").read() - -requirements = [ - "Django", - "iso8601", - "jsonschema", - "psycopg", - "pyjwt", - "pyld", - "python-dateutil", - "python-dotenv", - "pytz", - "requests", - "django-log-request-id", - "django-cors-headers", -] - -test_requirements = [ - "mock", - "model_bakery", - "pytest", - "pytest-django", - "pytest-mock", -] - - -setup( - name='catchpy', - version=version, - description="""Annotation storage backend""", - long_description=readme, - author='nmaekawa', - author_email='nmaekawa@g.harvard.edu', - url='https://github.com/nmaekawa/catchpy', - packages=find_packages(exclude=["docs", "tests*"]), - package_data={ - 'anno': ['static/anno/*.json'], - }, - install_requires=requirements, - tests_require=test_requirements, - zip_safe=False, - keywords='catchpy', - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Framework :: Django', - 'Framework :: Django :: 4.2', - 'Intended Audience :: Developers', - 'Natural Language :: English', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - ], -) diff --git a/test.Dockerfile b/test.Dockerfile index 8503cd0..1216bd0 100644 --- a/test.Dockerfile +++ b/test.Dockerfile @@ -1,27 +1,13 @@ -FROM python:3.11 +# pass in Python version as build arg to allow for tests to be run on multiple versions of Python +ARG PYTHON_VERSION=3.11 +FROM python:${PYTHON_VERSION} +ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1 - -RUN apt-get update # Include fortune library for quote generation for text annotations -RUN apt-get install fortune-mod -y +RUN apt-get update && apt-get install -y fortune-mod ENV PATH "$PATH:/usr/games" -# Install all other versions of Python we want to test with tox -RUN git clone https://github.com/pyenv/pyenv /root/.pyenv -RUN for PYTHON_VERSION in 3.8.17 3.9.17 3.10.12 3.11.4 3.12.0b4; do \ - set -ex \ - && /root/.pyenv/bin/pyenv install ${PYTHON_VERSION} \ - && /root/.pyenv/versions/${PYTHON_VERSION}/bin/python -m pip install --upgrade pip \ - ; done - -# Add to PATH, in order of lowest precedence to highest. -ENV PATH /root/.pyenv/versions/3.8.17/bin:${PATH} -ENV PATH /root/.pyenv/versions/3.9.17/bin:${PATH} -ENV PATH /root/.pyenv/versions/3.10.12/bin:${PATH} -ENV PATH /root/.pyenv/versions/3.12.0b4/bin:${PATH} -ENV PATH /root/.pyenv/versions/3.11.4/bin:${PATH} - RUN mkdir /code WORKDIR /code ADD . /code