├── .coveragerc ├── .editorconfig ├── .envrc-example ├── .fussyfox.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── feature_request.yaml ├── dependabot.yml └── workflows │ ├── auto-approve.yml │ ├── auto-merge.yml │ ├── codacy-analysis.yml │ ├── codeql.yml │ ├── gh-page.yml │ ├── greetings.yml │ ├── lint.yml │ ├── release.yml │ ├── test.yml │ └── update-doc-assets.yml ├── .gitignore ├── .pep8speaks.yml ├── .pre-commit-config.yaml ├── .pyup.yml ├── .readthedocs.yml ├── .whitesource ├── AUTHORS ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── SPECS ├── demo ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── subscription.py └── tests.py ├── django_model_subscription ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── docs ├── .nojekyll ├── Makefile ├── make.bat └── source │ ├── changelog.rst │ ├── conf.py │ ├── configuration.rst │ ├── contents.rst │ ├── index.rst │ ├── installation.rst │ ├── model_subscription.rst │ ├── register_subscribers.rst │ ├── settings.rst │ ├── subscribers.rst │ └── subscription_model.rst ├── manage.py ├── model_subscription ├── __init__.py ├── apps.py ├── constants.py ├── decorators.py ├── mixin.py ├── models.py ├── observers.py ├── subscriber.py ├── tests.py ├── types.py └── utils.py ├── mypy.ini ├── poetry.lock ├── pyproject.toml ├── renovate.json ├── setup.cfg └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = model_subscription 3 | omit = model_subscription/test* 4 | 5 | [report] 6 | precision = 1 7 | exclude_lines = 8 | pragma: no cover 9 | pass 10 | def __repr__ 11 | if self.debug: 12 | if settings.DEBUG 13 | # Don't complain if tests don't hit defensive assertion code: 14 | raise AssertionError 15 | raise NotImplementedError 16 | except ImportError 17 | except IntegrityError 18 | # Don't complain if non-runnable code isn't run: 19 | if __name__ == .__main__.: 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.envrc-example: -------------------------------------------------------------------------------- 1 | export PG_DB=demo_postgres 2 | export PG_USER=test_user 3 | export MSQL_DB=demo_mysql 4 | export MSQL_USER=test_user 5 | export MSQL_PASSWORD=Test_user_password@123 6 | -------------------------------------------------------------------------------- /.fussyfox.yml: -------------------------------------------------------------------------------- 1 | ## list of checks you would like to run on this repository 2 | # e.g.: 3 | - flake8 4 | - black 5 | # - pycodestyle 6 | # - pydocstyle 7 | # - pyflakes 8 | # - bandit 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jackton1 4 | patreon: # Replace with a single Patreon username 5 | open_collective: tj-django 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: [] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug 2 | description: Create a report to help us improve 3 | title: "[BUG] " 4 | labels: [bug, needs triage] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: checkboxes 12 | attributes: 13 | label: Is there an existing issue for this? 14 | description: Please search to see if an issue already exists for the bug you encountered. 15 | options: 16 | - label: I have searched the existing issues 17 | required: true 18 | - type: checkboxes 19 | attributes: 20 | label: Does this issue exist in the latest version? 21 | description: Please view all releases to confirm that this issue hasn't already been fixed. 22 | options: 23 | - label: I'm using the latest release 24 | required: true 25 | - type: textarea 26 | id: what-happened 27 | attributes: 28 | label: Describe the bug? 29 | description: A clear and concise description of what the bug is 30 | placeholder: Tell us what you see! 31 | validations: 32 | required: true 33 | - type: textarea 34 | id: reproduce 35 | attributes: 36 | label: To Reproduce 37 | description: Steps to reproduce the behavior? 38 | placeholder: | 39 | 1. In this environment... 40 | 2. With this config... 41 | 3. Run '...' 42 | 4. See error... 43 | validations: 44 | required: true 45 | - type: dropdown 46 | id: os 47 | attributes: 48 | label: What OS are you seeing the problem on? 49 | multiple: true 50 | options: 51 | - Ubuntu 52 | - macOS 53 | - Windows 54 | - Other 55 | validations: 56 | required: false 57 | - type: textarea 58 | id: expected 59 | attributes: 60 | label: Expected behavior? 61 | description: A clear and concise description of what you expected to happen. 62 | placeholder: Tell us what you expected! 63 | validations: 64 | required: true 65 | - type: textarea 66 | id: logs 67 | attributes: 68 | label: Relevant log output 69 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 70 | placeholder: | 71 | This can be achieved by: 72 | 1. Re-running the workflow with debug logging enabled. 73 | 2. Copy or download the log archive. 74 | 3. Paste the contents here or upload the file in a subsequent comment. 75 | render: shell 76 | - type: textarea 77 | attributes: 78 | label: Anything else? 79 | description: | 80 | Links? or References? 81 | 82 | Anything that will give us more context about the issue you are encountering! 83 | 84 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 85 | validations: 86 | required: false 87 | - type: checkboxes 88 | id: terms 89 | attributes: 90 | label: Code of Conduct 91 | description: By submitting this issue, you agree to follow our [Code of Conduct](../blob/main/CODE_OF_CONDUCT.md) 92 | options: 93 | - label: I agree to follow this project's Code of Conduct 94 | required: true 95 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: "[Feature] <title>" 4 | labels: [enhancement] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this feature request! 11 | - type: checkboxes 12 | attributes: 13 | label: Is this feature missing in the latest version? 14 | description: Please upgrade to the latest version to verify that this feature is still missing. 15 | options: 16 | - label: I'm using the latest release 17 | required: true 18 | - type: textarea 19 | id: what-happened 20 | attributes: 21 | label: Is your feature request related to a problem? Please describe. 22 | description: | 23 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 24 | placeholder: Tell us what you see! 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: requests 29 | attributes: 30 | label: Describe the solution you'd like? 31 | description: A clear and concise description of what you want to happen. 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: alternative 36 | attributes: 37 | label: Describe alternatives you've considered? 38 | description: A clear and concise description of any alternative solutions or features you've considered. 39 | validations: 40 | required: false 41 | - type: textarea 42 | attributes: 43 | label: Anything else? 44 | description: | 45 | Links? or References? 46 | 47 | Add any other context or screenshots about the feature request here. 48 | 49 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 50 | validations: 51 | required: false 52 | - type: checkboxes 53 | id: terms 54 | attributes: 55 | label: Code of Conduct 56 | description: By submitting this issue, you agree to follow our [Code of Conduct](./CODE_OF_CONDUCT.md) 57 | options: 58 | - label: I agree to follow this project's Code of Conduct 59 | required: true 60 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | labels: 9 | - "dependencies" 10 | - "dependabot" 11 | - "automerge" 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: daily 16 | open-pull-requests-limit: 10 17 | labels: 18 | - "dependencies" 19 | - "dependabot" 20 | - "automerge" 21 | -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | name: Auto approve 2 | 3 | on: 4 | pull_request_target 5 | 6 | jobs: 7 | auto-approve: 8 | runs-on: ubuntu-latest 9 | if: | 10 | ( 11 | github.event.pull_request.user.login == 'dependabot[bot]' || 12 | github.event.pull_request.user.login == 'dependabot' || 13 | github.event.pull_request.user.login == 'dependabot-preview[bot]' || 14 | github.event.pull_request.user.login == 'dependabot-preview' || 15 | github.event.pull_request.user.login == 'renovate[bot]' || 16 | github.event.pull_request.user.login == 'renovate' || 17 | github.event.pull_request.user.login == 'github-actions[bot]' || 18 | github.event.pull_request.user.login == 'pre-commit-ci' || 19 | github.event.pull_request.user.login == 'pre-commit-ci[bot]' 20 | ) 21 | && 22 | ( 23 | github.actor == 'dependabot[bot]' || 24 | github.actor == 'dependabot' || 25 | github.actor == 'dependabot-preview[bot]' || 26 | github.actor == 'dependabot-preview' || 27 | github.actor == 'renovate[bot]' || 28 | github.actor == 'renovate' || 29 | github.actor == 'github-actions[bot]' || 30 | github.actor == 'pre-commit-ci' || 31 | github.actor == 'pre-commit-ci[bot]' 32 | ) 33 | steps: 34 | - uses: hmarr/auto-approve-action@v2 35 | with: 36 | github-token: ${{ secrets.PAT_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | on: 3 | check_suite: 4 | types: 5 | - completed 6 | 7 | jobs: 8 | automerge: 9 | runs-on: ubuntu-latest 10 | if: | 11 | github.actor == 'dependabot[bot]' || 12 | github.actor == 'dependabot' || 13 | github.actor == 'dependabot-preview[bot]' || 14 | github.actor == 'dependabot-preview' || 15 | github.actor == 'renovate[bot]' || 16 | github.actor == 'renovate' 17 | steps: 18 | - name: automerge 19 | uses: pascalgn/automerge-action@v0.15.6 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 22 | MERGE_METHOD: "rebase" 23 | UPDATE_METHOD: "rebase" 24 | MERGE_RETRIES: "6" 25 | MERGE_RETRY_SLEEP: "100000" 26 | MERGE_LABELS: "" 27 | -------------------------------------------------------------------------------- /.github/workflows/codacy-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow checks out code, performs a Codacy security scan 2 | # and integrates the results with the 3 | # GitHub Advanced Security code scanning feature. For more information on 4 | # the Codacy security scan action usage and parameters, see 5 | # https://github.com/codacy/codacy-analysis-cli-action. 6 | # For more information on Codacy Analysis CLI in general, see 7 | # https://github.com/codacy/codacy-analysis-cli. 8 | 9 | name: Codacy Security Scan 10 | 11 | on: 12 | push: 13 | branches: [ main ] 14 | pull_request: 15 | # The branches below must be a subset of the branches above 16 | branches: [ main ] 17 | schedule: 18 | - cron: '15 16 * * 2' 19 | 20 | jobs: 21 | codacy-security-scan: 22 | name: Codacy Security Scan 23 | runs-on: ubuntu-latest 24 | steps: 25 | # Checkout the repository to the GitHub Actions runner 26 | - name: Checkout code 27 | uses: actions/checkout@v3 28 | 29 | # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis 30 | - name: Run Codacy Analysis CLI 31 | uses: codacy/codacy-analysis-cli-action@v4.3.0 32 | with: 33 | # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository 34 | # You can also omit the token and run the tools that support default configurations 35 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 36 | verbose: true 37 | output: results.sarif 38 | format: sarif 39 | # Adjust severity of non-security issues 40 | gh-code-scanning-compat: true 41 | # Force 0 exit code to allow SARIF file generation 42 | # This will handover control about PR rejection to the GitHub side 43 | max-allowed-issues: 2147483647 44 | 45 | # Upload the SARIF file generated in the previous step 46 | - name: Upload SARIF results file 47 | uses: github/codeql-action/upload-sarif@v2 48 | with: 49 | sarif_file: results.sarif 50 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "24 2 * * 1" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/gh-page.yml: -------------------------------------------------------------------------------- 1 | name: Github pages 2 | 3 | on: 4 | release: 5 | types: [published] 6 | push: 7 | tags: 8 | - '*' 9 | 10 | jobs: 11 | 12 | build_docs: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3.5.3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Python 3.6 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: 3.6 24 | 25 | - name: Pip cache 26 | uses: actions/cache@v3.3.1 27 | id: pip-cache 28 | with: 29 | path: ~/.cache/pip 30 | key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} 31 | restore-keys: | 32 | ${{ runner.os }}-pip 33 | 34 | - name: Install dependencies 35 | run: | 36 | pip install --upgrade pip setuptools 37 | sudo apt-get install -y --no-install-recommends python3-sphinx 38 | make install-dev 39 | 40 | - name: Generate docs 41 | run: | 42 | poetry run make build-docs 43 | 44 | - name: Update gh-pages branch 45 | run: | 46 | export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) 47 | 48 | ####################### 49 | # Update GitHub Pages # 50 | ####################### 51 | 52 | git config --global user.name "${GITHUB_ACTOR}" 53 | git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" 54 | 55 | docroot=`mktemp -d` 56 | rsync -av "docs/build/html/" "${docroot}/" 57 | 58 | pushd "${docroot}" || exit 59 | 60 | touch .nojekyll 61 | 62 | # don't bother maintaining history; just generate fresh 63 | git init 64 | git remote add deploy "https://token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" 65 | git checkout -b gh-pages 66 | 67 | git add . 68 | 69 | # commit all the new files 70 | msg="Updating Docs for commit ${GITHUB_SHA} made on `date -d"@${SOURCE_DATE_EPOCH}" --iso-8601=seconds` from ${GITHUB_REF} by ${GITHUB_ACTOR}" 71 | git commit -am "${msg}" 72 | 73 | # overwrite the contents of the gh-pages branch on our github.com repo 74 | git push deploy gh-pages --force 75 | 76 | popd || exit # return to main repo sandbox root 77 | env: 78 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 79 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: "Thanks for reporting this issue, don't forget to star this project if you haven't already to help us reach a wider audience." 13 | pr-message: "Thanks for implementing a fix, could you ensure that the test covers your changes if applicable." 14 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Run linters 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | lint: 9 | runs-on: ubuntu-latest 10 | if: github.actor != 'dependabot[bot]' && github.actor != 'dependabot' 11 | strategy: 12 | matrix: 13 | python-version: [3.5, 3.6, 3.7, 3.8, 3.9] 14 | 15 | steps: 16 | - uses: actions/checkout@v3.5.3 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - uses: actions/cache@v3.3.1 22 | id: pip-cache 23 | with: 24 | path: ~/.cache/pip 25 | key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }} 26 | restore-keys: | 27 | ${{ runner.os }}-pip-${{ matrix.python-version }}- 28 | - name: Install dependencies 29 | run: | 30 | pip install -U pip 31 | pip install flake8==3.8.4 32 | - name: Install black 33 | if: ${{ matrix.python-version != '3.5' }} 34 | run: | 35 | pip install black 36 | - name: Run Lint 37 | uses: wearerequired/lint-action@v2.3.0 38 | with: 39 | github_token: ${{ secrets.github_token }} 40 | black: ${{ matrix.python-version != '3.5' }} 41 | flake8: true 42 | git_email: "github-action[bot]@github.com" 43 | auto_fix: ${{ matrix.python-version != '3.5' }} 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create New Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3.5.3 14 | - name: Create Release 15 | uses: softprops/action-gh-release@v1 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: django model subscription test. 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | - '**' 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: true 17 | matrix: 18 | platform: [ubuntu-latest, macos-latest, windows-latest] 19 | python-version: [3.7, 3.8, 3.9, '3.10', 3.11] 20 | exclude: 21 | - platform: macos-latest 22 | python-version: 3.11 23 | - platform: windows-latest 24 | python-version: 3.11 25 | 26 | services: 27 | postgres: 28 | image: postgres:latest 29 | env: 30 | POSTGRES_USER: test_user 31 | POSTGRES_PASSWORD: test_user_password 32 | POSTGRES_DB: test 33 | ports: 34 | - 5432:5432 35 | # needed because the postgres container does not provide a healthcheck 36 | options: >- 37 | --health-cmd pg_isready 38 | --health-interval=10s 39 | --health-timeout=5s 40 | --health-retries=5 41 | 42 | mysql: 43 | image: bitnami/mysql:latest 44 | env: 45 | MYSQL_AUTHENTICATION_PLUGIN: mysql_native_password 46 | MYSQL_USER: test_user 47 | MYSQL_PASSWORD: test_user_password 48 | MYSQL_DATABASE: test 49 | MYSQL_ROOT_PASSWORD: test_user_password 50 | ports: 51 | - 3306:3306 52 | options: >- 53 | --health-cmd="mysqladmin ping" 54 | --health-interval=10s 55 | --health-timeout=5s 56 | --health-retries=5 57 | 58 | steps: 59 | - uses: actions/checkout@v3.5.3 60 | - name: Set up Python ${{ matrix.python-version }} 61 | uses: actions/setup-python@v4 62 | with: 63 | python-version: ${{ matrix.python-version }} 64 | 65 | - name: Pip cache 66 | uses: actions/cache@v3.3.1 67 | id: pip-cache 68 | with: 69 | path: ~/.cache/pip 70 | key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }} 71 | restore-keys: | 72 | ${{ runner.os }}-pip 73 | 74 | - name: Install dependencies 75 | run: | 76 | pip install --upgrade pip setuptools 77 | sudo apt-get install -y --no-install-recommends libpq-dev 78 | 79 | - name: Test with tox 80 | run: | 81 | make tox 82 | env: 83 | # use postgres for the host here because we have specified a container for the job. 84 | # If we were running the job on the VM this would be localhost 85 | POSTGRES_HOST: postgres 86 | POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} 87 | PG_USER: test_user 88 | PG_PASSWORD: test_user_password 89 | PG_DB: test 90 | MYSQL_DB: test 91 | MYSQL_USER: test_user 92 | MYSQL_PASSWORD: test_user_password 93 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} 94 | 95 | # - name: Upload coverage to Codecov 96 | # uses: codecov/codecov-action@v2.1.0 97 | # with: 98 | # fail_ci_if_error: true 99 | -------------------------------------------------------------------------------- /.github/workflows/update-doc-assets.yml: -------------------------------------------------------------------------------- 1 | name: Update doc assets. 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | sync-doc-assets: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3.5.3 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Run remark 17 | uses: tj-actions/remark@v3 18 | 19 | - name: Verify Changed files 20 | uses: tj-actions/verify-changed-files@v10 21 | id: verify_changed_files 22 | with: 23 | files: | 24 | README.md 25 | 26 | - name: README.md changed 27 | if: steps.verify_changed_files.outputs.files_changed == 'true' 28 | run: | 29 | echo "README.md has uncommited changes" 30 | exit 1 31 | - name: Create Pull Request 32 | if: failure() 33 | uses: peter-evans/create-pull-request@v4 34 | with: 35 | base: "main" 36 | title: "Updated README.md" 37 | branch: "chore/update-readme" 38 | commit-message: "Updated README.md" 39 | body: "Updated README.md" 40 | token: ${{ secrets.PAT_TOKEN }} 41 | 42 | - name: Copy CHANGELOG 43 | uses: docker://pandoc/core:2.19 44 | with: 45 | args: -t markdown CHANGELOG.md -t rst --output=docs/source/changelog.rst 46 | 47 | - name: Verify changed files 48 | id: changed_files 49 | uses: tj-actions/verify-changed-files@v10 50 | with: 51 | files: | 52 | docs/source/changelog.rst 53 | 54 | - name: Create Pull Request 55 | uses: peter-evans/create-pull-request@v4.2.4 56 | if: steps.changed_files.outputs.files_changed == 'true' 57 | with: 58 | commit-message: Synced README changes to docs 59 | committer: github-actions[bot] <github-actions[bot]@users.noreply@github.com> 60 | author: github-actions[bot] <github-actions[bot]@users.noreply.github.com> 61 | branch: chore/update-docs 62 | base: main 63 | delete-branch: true 64 | title: Updated docs 65 | body: | 66 | Updated docs 67 | - Auto-generated by github-actions[bot] 68 | assignees: jackton1 69 | reviewers: jackton1 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | *.sqlite3 4 | *.pyc 5 | __pycache__/ 6 | 7 | *.egg-info/ 8 | 9 | .vscode 10 | 11 | dist/ 12 | build/ 13 | 14 | _build 15 | _static 16 | _templates 17 | 18 | 19 | .tox/ 20 | .envrc 21 | 22 | .mypy_cache/ 23 | -------------------------------------------------------------------------------- /.pep8speaks.yml: -------------------------------------------------------------------------------- 1 | scanner: 2 | diff_only: True # If False, the entire file touched by the Pull Request is scanned for errors. If True, only the diff is scanned. 3 | linter: pycodestyle 4 | 5 | pycodestyle: # Same as scanner.linter value. Other option is flake8 6 | max-line-length: 100 # Default is 79 in PEP 8 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/PyCQA/autoflake 3 | rev: v2.1.1 4 | hooks: 5 | - id: autoflake 6 | args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variable'] 7 | 8 | - repo: https://github.com/pycqa/isort 9 | rev: 5.12.0 10 | hooks: 11 | - id: isort 12 | args: ["--profile", "black", "--filter-files"] 13 | 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v4.4.0 16 | hooks: 17 | - id: trailing-whitespace 18 | exclude: ^docs/.*|.*.md 19 | - id: end-of-file-fixer 20 | exclude: ^docs/.*|.*.md 21 | 22 | - repo: https://github.com/psf/black 23 | rev: 23.3.0 24 | hooks: 25 | - id: black 26 | language_version: python3 27 | 28 | - repo: https://github.com/adrienverge/yamllint.git 29 | rev: v1.32.0 30 | hooks: 31 | - id: yamllint 32 | args: ["-d", "relaxed"] 33 | 34 | - repo: https://github.com/pycqa/flake8 35 | rev: 6.0.0 36 | hooks: 37 | - id: flake8 38 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # autogenerated pyup.io config file 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | schedule: '' 5 | update: False 6 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | build: 9 | image: latest 10 | 11 | # Build documentation in the docs/ directory with Sphinx 12 | sphinx: 13 | configuration: docs/source/conf.py 14 | 15 | # Optionally set the version of Python and requirements required to build your docs 16 | python: 17 | version: 3.7 18 | install: 19 | - requirements: requirements.txt 20 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "LOW" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | GitHub <noreply@github.com> 2 | Tonye Jack <jtonye@ymail.com> 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [v0.2.2](https://github.com/tj-django/django-model-subscription/releases/tag/v0.2.2) - 2021-09-19 8 | 9 | <small>[Compare with v0.2.1](https://github.com/tj-django/django-model-subscription/compare/v0.2.1...v0.2.2)</small> 10 | 11 | ### Added 12 | - Added .pre-commit-config.yaml ([16061eb](https://github.com/tj-django/django-model-subscription/commit/16061ebef0760f32a97bbf9fbba7e31e074a5ac2) by Tonye Jack). 13 | - Added .github/workflows/codacy-analysis.yml ([726def6](https://github.com/tj-django/django-model-subscription/commit/726def6b516c2449fa89919df4e1c501df64c6e2) by Tonye Jack). 14 | - Added support for github actions image (#221) ([e3cb443](https://github.com/tj-django/django-model-subscription/commit/e3cb4431644f5bf6dbd733d71681539b2dd7ca70) by Tonye Jack). 15 | - Added .github/issue_template/bug_report.yaml ([b8f53c6](https://github.com/tj-django/django-model-subscription/commit/b8f53c66aac77326e91ac1a3818077492e2d097c) by Tonye Jack). 16 | - Added code_of_conduct.md ([e520555](https://github.com/tj-django/django-model-subscription/commit/e5205554b3dca0900d1350a5fe5162e4b1fc872a) by Tonye Jack). 17 | - Added .github/issue_template/feature_request.yaml ([c59fbd7](https://github.com/tj-django/django-model-subscription/commit/c59fbd7c36bd922005d1f14f8d1fe1e676b55fae) by Tonye Jack). 18 | - Added .pep8speaks.yml ([8791127](https://github.com/tj-django/django-model-subscription/commit/8791127663b79c6d804bcaf2713bd4e7443bbf7b) by Tonye Jack). 19 | - Added .whitesource ([c98428f](https://github.com/tj-django/django-model-subscription/commit/c98428f99500edfea6bbf0149cab2bf295683674) by Tonye Jack). 20 | - Added .github/workflow/auto-merge.yml ([ffa3288](https://github.com/tj-django/django-model-subscription/commit/ffa328834ec4deebd66e746516cb64e07127a504) by Tonye Jack). 21 | - Added .github/workflow/auto-approve.yml ([a4a558a](https://github.com/tj-django/django-model-subscription/commit/a4a558ab10cbbcdf9cfca5070d64d4f3476c0b4e) by Tonye Jack). 22 | - Added .github/workspace/automerge.yml ([8ad6b30](https://github.com/tj-django/django-model-subscription/commit/8ad6b3049744c8875f6d548a6c8d4e46e6d50edf) by Tonye Jack). 23 | - Added .github/automerge.yml ([d700ccf](https://github.com/tj-django/django-model-subscription/commit/d700ccf9cb52baeca7c585266abdb215f6769aa6) by Tonye Jack). 24 | - Added pre-commit-config.yaml ([309900b](https://github.com/tj-django/django-model-subscription/commit/309900b777aff080014f039f90f1ce7a543b1fd8) by Tonye Jack). 25 | - Added .editorconfig ([3ed1f9b](https://github.com/tj-django/django-model-subscription/commit/3ed1f9b3efbe61429796acafb51c0d281b05a826) by Tonye Jack). 26 | - Added .github/workflows/release.yml ([9af60a6](https://github.com/tj-django/django-model-subscription/commit/9af60a6ccfad50479d78b675968c6959ac43d9ce) by Tonye Jack). 27 | - Added .github/workflows/sync-release-version.yml ([6632118](https://github.com/tj-django/django-model-subscription/commit/6632118148606c416f5219272c592730d288f838) by Tonye Jack). 28 | - Added .github/issue_template/bug_report.md ([1930696](https://github.com/tj-django/django-model-subscription/commit/193069696e08d52f9adcb63a49cbfc2182d6a467) by Tonye Jack). 29 | - Added .github/issue_template/feature_request.md ([ab2723d](https://github.com/tj-django/django-model-subscription/commit/ab2723dc93197ab4ef247b9ddf662b7e0b62738f) by Tonye Jack). 30 | - Added .github/auto-approve.yml ([e1c0528](https://github.com/tj-django/django-model-subscription/commit/e1c05286240e6a68561f067cdc95b39c4d309ed5) by Tonye Jack). 31 | - Added .github/workflows/auto-approve.yml ([d8d1dd5](https://github.com/tj-django/django-model-subscription/commit/d8d1dd59f0f4f7f2db83fbf94075d9f9ceacb6cc) by Tonye Jack). 32 | 33 | ### Fixed 34 | - Fix: requirements-dev.txt to reduce vulnerabilities (#247) ([7b76394](https://github.com/tj-django/django-model-subscription/commit/7b76394702559831bf92f662c0e5afdcfdf73002) by Snyk bot). 35 | 36 | 37 | ## [v0.2.1](https://github.com/tj-django/django-model-subscription/releases/tag/v0.2.1) - 2021-03-21 38 | 39 | <small>[Compare with v0.2.0](https://github.com/tj-django/django-model-subscription/compare/v0.2.0...v0.2.1)</small> 40 | 41 | ### Added 42 | - Added support for coverage reporting. (#86) ([62b2a58](https://github.com/tj-django/django-model-subscription/commit/62b2a5812a0e1626d56054885ef754a7b1800f2e) by Tonye Jack). 43 | 44 | ### Fixed 45 | - Fixed docs. ([d85f4ba](https://github.com/tj-django/django-model-subscription/commit/d85f4ba6b998736971fa56830dbb69462a5353a3) by Tonye Jack). 46 | 47 | 48 | ## [v0.2.0](https://github.com/tj-django/django-model-subscription/releases/tag/v0.2.0) - 2021-03-03 49 | 50 | <small>[Compare with v0.1.0](https://github.com/tj-django/django-model-subscription/compare/v0.1.0...v0.2.0)</small> 51 | 52 | ### Fixed 53 | - Fixed installing psycopg2 on mac. ([16a487a](https://github.com/tj-django/django-model-subscription/commit/16a487af178399df5f4b04bfe15c82249bc622ac) by Tonye Jack). 54 | 55 | 56 | ## [v0.1.0](https://github.com/tj-django/django-model-subscription/releases/tag/v0.1.0) - 2021-02-28 57 | 58 | <small>[Compare with v0.0.10](https://github.com/tj-django/django-model-subscription/compare/v0.0.10...v0.1.0)</small> 59 | 60 | ### Fixed 61 | - Fixed release. ([e832ae7](https://github.com/tj-django/django-model-subscription/commit/e832ae7159310ede9188deb70c299d2fa648e3ec) by Tonye Jack). 62 | - Fixed install step. (#61) ([3c2810b](https://github.com/tj-django/django-model-subscription/commit/3c2810b2e0c4cc9c03e4dab45af1b7d9cef2d37b) by Tonye Jack). 63 | - Fixed error generating docs. (#57) ([3bfa8ba](https://github.com/tj-django/django-model-subscription/commit/3bfa8babf87c305da23fc1ffc956ef31b788f6d3) by Tonye Jack). 64 | - Fixed test ([04405cc](https://github.com/tj-django/django-model-subscription/commit/04405cc51cde8dcfd9eff893d473fab4eeb76a29) by Tonye Jack). 65 | - Fixed test. ([05a404a](https://github.com/tj-django/django-model-subscription/commit/05a404a4ccf99e7968bdf062947b196446ff0db9) by Tonye Jack). 66 | - Fixed type errors. ([8e71864](https://github.com/tj-django/django-model-subscription/commit/8e718648bd4da553234952fee21e71dd5701a72a) by Tonye Jack). 67 | - Fixed bug with docs. ([db6d7ab](https://github.com/tj-django/django-model-subscription/commit/db6d7ab584975ffdcd227fd75e1e7f14c78634fa) by Tonye Jack). 68 | - Fixed flake8 errors. ([07a4419](https://github.com/tj-django/django-model-subscription/commit/07a441925b37dc8580c968d92e5fc29de2ff2213) by Tonye Jack). 69 | 70 | ### Removed 71 | - Removed unused readme.rst. (#67) ([8564f0e](https://github.com/tj-django/django-model-subscription/commit/8564f0ef1b17c0b30196dbc4760f884e5b23090a) by Tonye Jack). 72 | - Removed unused code. ([896e520](https://github.com/tj-django/django-model-subscription/commit/896e520a09a049225080c4d77f2dd90d3fa16d60) by Tonye Jack). 73 | - Removed unused imports. ([dceea6c](https://github.com/tj-django/django-model-subscription/commit/dceea6ca50f0ccbc29a2e608dfb63584feb5b308) by Tonye Jack). 74 | 75 | 76 | ## [v0.0.10](https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.10) - 2020-02-28 77 | 78 | <small>[Compare with v0.0.9](https://github.com/tj-django/django-model-subscription/compare/v0.0.9...v0.0.10)</small> 79 | 80 | ### Fixed 81 | - Fixed missing pg_config. ([bcffacc](https://github.com/tj-django/django-model-subscription/commit/bcffacc49983439e390bfe8bd9896122569f6dfb) by Tonye Jack). 82 | - Fixed permission error. ([9aec463](https://github.com/tj-django/django-model-subscription/commit/9aec463a803a8ca099e10377d10619f6dcdae461) by Tonye Jack). 83 | - Fixed workflow. ([c157de1](https://github.com/tj-django/django-model-subscription/commit/c157de1e6721d81eef00bb208fa0df3751767d00) by Tonye Jack). 84 | 85 | ### Removed 86 | - Removed unused import. ([3b3a952](https://github.com/tj-django/django-model-subscription/commit/3b3a9522c26d79bea4d8d819e171052ae6c07863) by Tonye Jack). 87 | 88 | 89 | ## [v0.0.9](https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.9) - 2019-11-19 90 | 91 | <small>[Compare with v0.0.6](https://github.com/tj-django/django-model-subscription/compare/v0.0.6...v0.0.9)</small> 92 | 93 | ### Fixed 94 | - Fixed link. ([60e75d5](https://github.com/tj-django/django-model-subscription/commit/60e75d5c49da134dabc187a1ad16468d3fc159d9) by Tonye Jack). 95 | 96 | 97 | ## [v0.0.6](https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.6) - 2019-11-04 98 | 99 | <small>[Compare with v0.0.5](https://github.com/tj-django/django-model-subscription/compare/v0.0.5...v0.0.6)</small> 100 | 101 | ### Added 102 | - Added env vars. ([5738df6](https://github.com/tj-django/django-model-subscription/commit/5738df696b061f4c7343e63d2bf4508090d26ef5) by Tonye Jack). 103 | - Added documentation and improved readme.md ([946eac6](https://github.com/tj-django/django-model-subscription/commit/946eac64bd4505fe6bd02da8eef6febb852c9ab4) by Tonye Jack). 104 | 105 | ### Fixed 106 | - Fixed connection errors. ([cc048ad](https://github.com/tj-django/django-model-subscription/commit/cc048ad111f9c53e57612b98027e217f68acb80d) by Tonye Jack). 107 | - Fixed settings. ([09c26e0](https://github.com/tj-django/django-model-subscription/commit/09c26e02a552163fd03e8749aa08610244f565b5) by Tonye Jack). 108 | - Fixed test command. ([ccb0d7a](https://github.com/tj-django/django-model-subscription/commit/ccb0d7a7c731755c3c0f9614a63015619befa9a8) by Tonye Jack). 109 | 110 | ### Removed 111 | - Removed sdist. ([e2dcdae](https://github.com/tj-django/django-model-subscription/commit/e2dcdaec4a60e3ebdfc76c21ae479900a2e5652d) by Tonye Jack). 112 | 113 | 114 | ## [v0.0.5](https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.5) - 2019-10-23 115 | 116 | <small>[Compare with v0.0.4](https://github.com/tj-django/django-model-subscription/compare/v0.0.4...v0.0.5)</small> 117 | 118 | ### Fixed 119 | - Fixed bug with modelsubscription and added new decorators. ([6c616ed](https://github.com/tj-django/django-model-subscription/commit/6c616edb8f27b3287b22289657d8e758d684f815) by Tonye Jack). 120 | 121 | 122 | ## [v0.0.4](https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.4) - 2019-10-21 123 | 124 | <small>[Compare with v0.0.3](https://github.com/tj-django/django-model-subscription/compare/v0.0.3...v0.0.4)</small> 125 | 126 | ### Added 127 | - Added model subscription to sdist. ([df9074e](https://github.com/tj-django/django-model-subscription/commit/df9074e715d894632b1dd97d4b59f7a13a05e622) by Tonye Jack). 128 | 129 | ### Removed 130 | - Removed unrelated settings. ([f270a93](https://github.com/tj-django/django-model-subscription/commit/f270a9360beaec4147bf107808a45b720b1c6d34) by Tonye Jack). 131 | 132 | 133 | ## [v0.0.3](https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.3) - 2019-10-21 134 | 135 | <small>[Compare with v0.0.1](https://github.com/tj-django/django-model-subscription/compare/v0.0.1...v0.0.3)</small> 136 | 137 | ### Removed 138 | - Removed .bumpversion.cfg ([2140477](https://github.com/tj-django/django-model-subscription/commit/2140477779916f3c7f1abc003116c6371a6ff8d0) by Tonye Jack). 139 | 140 | 141 | ## [v0.0.1](https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.1) - 2019-10-20 142 | 143 | <small>[Compare with first commit](https://github.com/tj-django/django-model-subscription/compare/fadbc19ce2b1307403e85a707d085e865bcfe453...v0.0.1)</small> 144 | 145 | ### Added 146 | - Added subscription and observers module. ([4405e25](https://github.com/tj-django/django-model-subscription/commit/4405e25da8b90d77aa0c4fe306836d5c4b7f7e41) by Tonye Jack). 147 | 148 | 149 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | jtonye@ymail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2019 Tonye Jack <tonyejck@gmail.com> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include model_subscription *.py 2 | include LICENSE 3 | include README.rst 4 | include README.md 5 | include *.rst 6 | 7 | recursive-exclude demo *.py 8 | recursive-exclude demo/migrations *.py 9 | recursive-exclude django_model_subscription *.py 10 | prune model_subscription/tests.py 11 | prune docs 12 | prune .github 13 | prune .circleci 14 | global-exclude __pycache__ 15 | global-exclude *.py[co] 16 | exclude *.example 17 | exclude *.json 18 | exclude .yamllint 19 | exclude *.png 20 | exclude *.egg-info 21 | exclude *.py 22 | exclude *.txt 23 | exclude .bumpversion.cfg 24 | exclude .coveragerc 25 | exclude Makefile 26 | exclude tox.ini 27 | exclude .tox 28 | exclude mypy.ini 29 | exclude *.json 30 | exclude *.lock 31 | exclude *.png 32 | exclude *.yml 33 | exclude AUTHORS 34 | exclude Makefile 35 | exclude SPECS 36 | exclude .all-contributorsrc 37 | exclude .pypirc 38 | exclude .envrc-example 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Self-Documented Makefile see https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 2 | 3 | .DEFAULT_GOAL := help 4 | 5 | PYTHON := /usr/bin/env python 6 | MANAGE_PY := $(PYTHON) manage.py 7 | PYTHON_PIP := /usr/bin/env pip 8 | PIP_COMPILE := /usr/bin/env pip-compile 9 | PART := patch 10 | DOCS_DIR := ./docs 11 | DOC_SOURCE_DIR := source 12 | DOC_BUILD_DIR := build 13 | DOC_SERVE_PORT := 8080 14 | 15 | # Put it first so that "make" without argument is like "make help". 16 | help: 17 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-32s-\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 18 | 19 | .PHONY: help 20 | 21 | guard-%: ## Checks that env var is set else exits with non 0 mainly used in CI; 22 | @if [ -z '${${*}}' ]; then echo 'Environment variable $* not set' && exit 1; fi 23 | 24 | # -------------------------------------------------------- 25 | # ------- Python package (pip) management commands ------- 26 | # -------------------------------------------------------- 27 | clean-build: ## Clean project build artifacts. 28 | @echo "Removing build assets..." 29 | @rm -rf build dist *.egg-info 30 | 31 | install: clean-build ## Install project dependencies. 32 | @echo "Installing project in dependencies..." 33 | @pip install -U pip setuptools poetry 34 | ifeq "$(shell uname)" "Darwin" 35 | @export LDFLAGS="-L/usr/local/opt/openssl/lib" 36 | @export CPPFLAGS="-I/usr/local/opt/openssl/include" 37 | endif 38 | @poetry install -vvv 39 | @poetry update 40 | 41 | install-dev: clean-build install ## Install development extra dependencies. 42 | @echo "Installing development requirements..." 43 | @poetry install -E "development" 44 | 45 | tag-build: 46 | @git tag v$(shell cat pyproject.toml | grep -E "^version" | sed 's/["= ]//g;s/version//g') 47 | 48 | release-to-pypi: increase-version tag-build ## Release project to pypi 49 | @poetry build 50 | @poetry publish 51 | @git-changelog . > CHANGELOG.md 52 | @git commit -am "Synced pyproject.toml and updated CHANGELOG.md." 53 | @git push --tags 54 | @git push 55 | 56 | # -------------------------------------------------------- 57 | # ----- Sphinx Documentation commands -------------------- 58 | # -------------------------------------------------------- 59 | build-docs: 60 | @echo "Building docs..." 61 | @$(MAKE) -C $(DOCS_DIR) SPHINXOPTS='-W' clean html 62 | 63 | github: 64 | @cd $(DOCS_DIR) && make github 65 | 66 | view-docs: build-docs ## Serve sphinx doc locally. 67 | @echo "Serving documentation..." 68 | @cd $(DOCS_DIR) && sphinx-autobuild $(DOC_SOURCE_DIR) $(DOC_BUILD_DIR) -p $(DOC_SERVE_PORT) 69 | 70 | # ---------------------------------------------------------- 71 | # ---------- Upgrade project version (bumpversion) -------- 72 | # ---------------------------------------------------------- 73 | increase-version: clean-build guard-PART ## Bump the project version (using the $PART env: defaults to 'patch'). 74 | @echo "Increasing project '$(PART)' version..." 75 | @poetry update 76 | @poetry version $(PART) 77 | 78 | # ---------------------------------------------------------- 79 | # --------- Run project Test ------------------------------- 80 | # ---------------------------------------------------------- 81 | tox: ## Run tox test 82 | @pip install "tox>=3.14" tox-gh-actions 83 | @tox 84 | 85 | clean-test-all: clean-build ## Clean build and test assets. 86 | @rm -rf .tox db.* .mypy_cache 87 | 88 | # ---------------------------------------------------------- 89 | # ---------- Managment Commands ---------------------------- 90 | # ---------------------------------------------------------- 91 | test: 92 | @python manage.py test --no-input 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI](https://img.shields.io/pypi/v/django-model-subscription)](https://pypi.org/project/django-model-subscription/) [![Actions Status](https://github.com/jackton1/django-model-subscription/workflows/django%20model%20subscription%20test./badge.svg)](https://github.com/jackton1/django-model-subscription/actions?query=workflow%3A"django+model+subscription+test.") 2 | [![Documentation Status](https://readthedocs.org/projects/django-model-subscription/badge/?version=latest)](https://django-model-subscription.readthedocs.io/en/latest/?badge=latest) 3 | 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/353aa86af402423cbcd4e810bca664cc)](https://www.codacy.com/gh/tj-django/django-model-subscription/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-model-subscription\&utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/353aa86af402423cbcd4e810bca664cc)](https://www.codacy.com/gh/tj-django/django-model-subscription/dashboard?utm_source=github.com\&utm_medium=referral\&utm_content=tj-django/django-model-subscription\&utm_campaign=Badge_Coverage) [![codecov](https://codecov.io/gh/tj-django/django-model-subscription/branch/master/graph/badge.svg?token=P5X3FM234E)](https://codecov.io/gh/tj-django/django-model-subscription) [![PyPI - License](https://img.shields.io/pypi/l/django-model-subscription.svg)](https://github.com/jackton1/django-model-subscription/blob/master/LICENSE) 5 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/tj-django/django-model-subscription/main.svg)](https://results.pre-commit.ci/latest/github/tj-django/django-model-subscription/main) 6 | 7 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-model-subscription.svg)](https://pypi.org/project/django-model-subscription) 8 | [![PyPI - Django Version](https://img.shields.io/pypi/djversions/django-model-subscription.svg)](https://docs.djangoproject.com/en/3.2/releases/) 9 | [![Downloads](https://pepy.tech/badge/django-model-subscription)](https://pepy.tech/project/django-model-subscription) 10 | 11 | # django-model-subscription 12 | 13 | Subscribe to django model changes include bulk create/update/delete. 14 | 15 | ## Table of contents 16 | 17 | * [Motivation](#Motivation) 18 | * [Installation](#Installation) 19 | * [Usage](#Usage) 20 | * [Decorators](#Decorators) 21 | * [Setup Subscribers using AppConfig.ready](#setup-subscribers-using-appconfigready-recomended) 22 | * [Setup Subscribers with auto discovery](#setup-subscribers-using-auto-discovery) 23 | * [Credits](#credits) 24 | * [Resources](#resources) 25 | 26 | ### Features 27 | 28 | * Using Observer Pattern notify subscribers about changes to a django model. 29 | * Decouple Business logic from Models.save 30 | * Support for bulk actions (Not available using django signals.) 31 | * Use noop subscribers when `settings.SUBSCRIPTION_DISABLE_SUBSCRIBERS` is `True` 32 | which prevents having to mock subscribers that call external services in testing or local development 33 | environments. 34 | * Show changes to the instance after it has been updated i.e diff's the initial state and the 35 | current state. 36 | 37 | <img width="580" alt="Subscriber" src="https://user-images.githubusercontent.com/17484350/139741273-83cd6400-552e-419f-8cca-0f13caacf5aa.png"> 38 | 39 | ### Installation 40 | 41 | ```bash 42 | $ pip install django-model-subscription 43 | ``` 44 | 45 | Add `model_subscription` to your INSTALLED\_APPS 46 | 47 | ```python 48 | INSTALLED_APPS = [ 49 | ..., 50 | 'model_subscription', 51 | ... 52 | ] 53 | ``` 54 | 55 | ### Usage 56 | 57 | ##### Using the `SubscriptionModelMixin` and `SubscriptionQuerySet` 58 | 59 | ```py 60 | from model_subscription.mixin import SubscriptionModelMixin 61 | from model_subscription.model import SubscriptionQuerySet 62 | 63 | 64 | class TestModel(SubscriptionModelMixin, models.Model): 65 | name = models.CharField(max_length=255) 66 | 67 | objects = SubscriptionQuerySet.as_manager() 68 | ``` 69 | 70 | ##### Subclassing the `SubscriptionModel` base class. 71 | 72 | ```py 73 | from model_subscription.model import SubscriptionModel 74 | 75 | 76 | class TestModel(SubscriptionModel): 77 | name = models.CharField(max_length=255) 78 | 79 | ``` 80 | 81 | #### Creating subscribers. 82 | 83 | * Using `OperationType` 84 | 85 | ```python 86 | import logging 87 | from model_subscription.decorators import subscribe 88 | from model_subscription.constants import OperationType 89 | 90 | log = logging.getLogger(__name__) 91 | 92 | @subscribe(OperationType.CREATE, TestModel) 93 | def handle_create(instance): 94 | log.debug('Created {}'.format(instance.name)) 95 | 96 | 97 | ``` 98 | 99 | * Using `create_subscription` directly (succinct version). 100 | 101 | ```python 102 | 103 | import logging 104 | from model_subscription.decorators import create_subscription 105 | 106 | log = logging.getLogger(__name__) 107 | 108 | @create_subscription(TestModel) 109 | def handle_create(instance): 110 | log.debug('Created {}'.format(instance.name)) 111 | 112 | 113 | ``` 114 | 115 | ### Decorators 116 | 117 | * `subscribe`: Explicit (Requires a valid OperationType). 118 | 119 | #### (Create, Update, Delete) operations. 120 | 121 | * `create_subscription`: Subscribes to create operation i.e a new instance. 122 | 123 | ```python 124 | @create_subscription(TestModel) 125 | def handle_create(instance): 126 | log.debug('1. Created {}'.format(instance.name)) 127 | ``` 128 | 129 | * `update_subscription`: Subscribes to updates also includes (`changed_data`). 130 | 131 | ```python 132 | @update_subscription(TestModel) 133 | def handle_update(instance, changed_data): 134 | log.debug('Updated {} {}'.format(instance.name, changed_data)) 135 | ``` 136 | 137 | * `delete_subscription`: Subscribes to delete operation: 138 | 139 | > NOTE: The instance.pk is already set to None. 140 | 141 | ```python 142 | @delete_subscription(TestModel) 143 | def handle_delete(instance): 144 | log.debug('Deleted {}'.format(instance.name)) 145 | ``` 146 | 147 | #### (Bulk Create, Bulk Update, Bulk Delete) operations. 148 | 149 | * `bulk_create_subscription`: Subscribe to bulk create operations. 150 | 151 | ```python 152 | 153 | @bulk_create_subscription(TestModel) 154 | def handle_bulk_create(instances): 155 | for instance in instances: 156 | log.debug('Bulk Created {}'.format(instance.name)) 157 | 158 | ``` 159 | 160 | * `bulk_update_subscription`: Subscribe to bulk update operations. 161 | 162 | ```python 163 | @bulk_update_subscription(TestModel) 164 | def handle_bulk_update(instances): 165 | for instance in instances: 166 | log.debug('Updated {}'.format(instance.name)) 167 | ``` 168 | 169 | * `bulk_delete_subscription`: Subscribe to bulk delete operations. 170 | 171 | ```python 172 | 173 | @bulk_delete_subscription(TestModel) 174 | def handle_bulk_delete(instances): 175 | for instance in instances: 176 | log.debug('Deleted {}'.format(instance.name)) 177 | 178 | ``` 179 | 180 | ### Setup Subscribers using AppConfig.ready `(Recomended)`. 181 | 182 | Update you `apps.py` 183 | 184 | ```python 185 | 186 | from django.apps import AppConfig 187 | 188 | 189 | class MyAppConfig(AppConfig): 190 | name = 'myapp' 191 | 192 | def ready(self): 193 | from myapp import subscriptions 194 | 195 | ``` 196 | 197 | ### Setup Subscribers using auto discovery. 198 | 199 | By default the `settings.SUBSCRIPTION_AUTO_DISCOVER` is set to `False`. 200 | 201 | To use auto discovery this is not recommended as it would notify the subscribers 202 | wherever the model is used i.e IPython notebook, external scripts. 203 | 204 | In your `settings.py` add 205 | 206 | ```python 207 | 208 | SUBSCRIPTION_AUTO_DISCOVER = True 209 | 210 | ``` 211 | 212 | #### Setting up the `SUBSCRIPTION_MODULE` 213 | 214 | > NOTE: This is only required when `SUBSCRIPTION_AUTO_DISCOVER = True` 215 | 216 | ```python 217 | 218 | SUBSCRIPTION_MODULE = 'subscription' 219 | ``` 220 | 221 | #### Credits 222 | 223 | * [django-lifecycle](https://github.com/rsinger86/django-lifecycle) 224 | 225 | If you feel generous and want to show some extra appreciation: 226 | 227 | [![Buy me a coffee][buymeacoffee-shield]][buymeacoffee] 228 | 229 | [buymeacoffee]: https://www.buymeacoffee.com/jackton1 230 | 231 | [buymeacoffee-shield]: https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png 232 | 233 | #### Resources 234 | 235 | * https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Observer.html 236 | * https://refactoring.guru/design-patterns/observer 237 | * https://hackernoon.com/observer-vs-pub-sub-pattern-50d3b27f838c 238 | 239 | ### TODO's 240 | 241 | * Supporting field level subscriptions. 242 | * Support class based subscribers which implements `__call__` 243 | * Extend to include custom OperationType. 244 | * Add support for using a single class to manage multiple actions i.e MyClass.update, MyClass.create. 245 | -------------------------------------------------------------------------------- /SPECS: -------------------------------------------------------------------------------- 1 | TODOS 2 | - Add support for field subscriptions i.e should this also include Foriegn keys and Many to Many 3 | relationships. 4 | - Investigate using https://github.com/django/django-docker-box for testing package across multiple database vendors. 5 | -------------------------------------------------------------------------------- /demo/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "demo.apps.DemoConfig" 2 | -------------------------------------------------------------------------------- /demo/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin # noqa 2 | -------------------------------------------------------------------------------- /demo/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DemoConfig(AppConfig): 5 | name = "demo" 6 | 7 | def ready(self): 8 | from demo import subscription # noqa: F401 9 | -------------------------------------------------------------------------------- /demo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2019-10-20 16:27 2 | 3 | from django.db import migrations, models 4 | 5 | import model_subscription.mixin 6 | 7 | 8 | class Migration(migrations.Migration): 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="TestModel", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("name", models.CharField(max_length=20)), 27 | ], 28 | options={ 29 | "abstract": False, 30 | }, 31 | bases=(model_subscription.mixin.SubscriptionModelMixin, models.Model), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /demo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj-django/django-model-subscription/1c076021bcdbc46867be14bb177bbe99cf84a0ed/demo/migrations/__init__.py -------------------------------------------------------------------------------- /demo/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | from model_subscription.models import SubscriptionModel 5 | 6 | 7 | class TestModel(SubscriptionModel): 8 | name = models.CharField(max_length=20) 9 | 10 | def __str__(self): 11 | return "<{}: {}>".format(self.pk, self.name) 12 | -------------------------------------------------------------------------------- /demo/subscription.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from demo.models import TestModel 4 | from model_subscription.constants import OperationType 5 | from model_subscription.decorators import ( 6 | bulk_create_subscription, 7 | bulk_delete_subscription, 8 | bulk_update_subscription, 9 | create_subscription, 10 | delete_subscription, 11 | subscribe, 12 | unsubscribe_create, 13 | update_subscription, 14 | ) 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | @subscribe(OperationType.CREATE, TestModel) 20 | def handle_create_1(instance): 21 | log.debug("1. Created {}".format(instance.name)) 22 | 23 | 24 | @create_subscription(TestModel) 25 | def handle_create_2(instance): 26 | log.debug("2. Created {}".format(instance.name)) 27 | 28 | 29 | unsubscribe_create(TestModel, handle_create_2) 30 | 31 | 32 | @bulk_create_subscription(TestModel) 33 | def handle_bulk_create(instances): 34 | for instance in instances: 35 | log.debug("Bulk Created {}".format(instance.name)) 36 | 37 | 38 | @create_subscription(TestModel) 39 | def handle_create_3(instance): 40 | log.debug("3. Created {}".format(instance.name)) 41 | 42 | 43 | @update_subscription(TestModel) 44 | def handle_update(instance, changed_data): 45 | log.debug("Updated {}".format(instance.name)) 46 | 47 | 48 | @bulk_update_subscription(TestModel) 49 | def handle_bulk_update(instances): 50 | for instance in instances: 51 | log.debug("Bulk Updated {}".format(instance.name)) 52 | 53 | 54 | @delete_subscription(TestModel) 55 | def handle_delete(instance): 56 | log.debug("Deleted {}".format(instance.name)) 57 | 58 | 59 | @bulk_delete_subscription(TestModel) 60 | def handle_bulk_delete(instances): 61 | for instance in instances: 62 | log.debug("Bulk Deleted {}".format(instance.name)) 63 | -------------------------------------------------------------------------------- /demo/tests.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj-django/django-model-subscription/1c076021bcdbc46867be14bb177bbe99cf84a0ed/demo/tests.py -------------------------------------------------------------------------------- /django_model_subscription/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.0.1" 2 | __url__ = "https://github.com/jackton1/django-model-subscription" 3 | __author__ = "Tonye Jack" 4 | __email__ = "tonyejck@gmail.com" 5 | -------------------------------------------------------------------------------- /django_model_subscription/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_model_subscription project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "py3w_x9wxg6ws^z7irzu#1f3wqu!*77!4wuc=1$l_lh(3-@0cg" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | "django.contrib.admin", 33 | "django.contrib.auth", 34 | "django.contrib.contenttypes", 35 | "django.contrib.sessions", 36 | "django.contrib.messages", 37 | "django.contrib.staticfiles", 38 | "model_subscription", 39 | "demo", 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | "django.middleware.security.SecurityMiddleware", 44 | "django.contrib.sessions.middleware.SessionMiddleware", 45 | "django.middleware.common.CommonMiddleware", 46 | "django.middleware.csrf.CsrfViewMiddleware", 47 | "django.contrib.auth.middleware.AuthenticationMiddleware", 48 | "django.contrib.messages.middleware.MessageMiddleware", 49 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 50 | ] 51 | 52 | TEMPLATES = [ 53 | { 54 | "BACKEND": "django.template.backends.django.DjangoTemplates", 55 | "DIRS": [], 56 | "APP_DIRS": True, 57 | "OPTIONS": { 58 | "context_processors": [ 59 | "django.template.context_processors.debug", 60 | "django.template.context_processors.request", 61 | "django.contrib.auth.context_processors.auth", 62 | "django.contrib.messages.context_processors.messages", 63 | ], 64 | }, 65 | }, 66 | ] 67 | 68 | WSGI_APPLICATION = "django_model_subscription.wsgi.application" 69 | 70 | # Database 71 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 72 | 73 | DATABASES = { 74 | "default": { 75 | "ENGINE": "django.db.backends.sqlite3", 76 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 77 | }, 78 | "postgres": { 79 | "ENGINE": "django.db.backends.postgresql", 80 | "NAME": os.getenv("PG_DB", "test"), 81 | "USER": os.getenv("PG_USER", "test_user"), 82 | "PASSWORD": os.getenv("PG_PASSWORD", "test_user_password"), 83 | "HOST": "localhost", 84 | "PORT": "5432", 85 | }, 86 | # "mysql": { 87 | # "ENGINE": "django.db.backends.mysql", 88 | # "NAME": os.getenv("MSQL_DB", "test"), 89 | # "HOST": "127.0.0.1", 90 | # "PORT": "3306", 91 | # "USER": os.getenv("MSQL_USER", "test_user"), 92 | # "PASSWORD": os.getenv("MSQL_PASSWORD", "test_user_password"), 93 | # }, 94 | } 95 | 96 | # Internationalization 97 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 98 | 99 | TIME_ZONE = "UTC" 100 | 101 | USE_I18N = True 102 | 103 | USE_L10N = True 104 | 105 | USE_TZ = True 106 | -------------------------------------------------------------------------------- /django_model_subscription/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | django_model_subscription URL Configuration 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path 19 | 20 | urlpatterns = [ 21 | path("admin/", admin.site.urls), 22 | ] 23 | -------------------------------------------------------------------------------- /django_model_subscription/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_model_subscription project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_model_subscription.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tj-django/django-model-subscription/1c076021bcdbc46867be14bb177bbe99cf84a0ed/docs/.nojekyll -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | github: 17 | @make html 18 | @cp -a build/html/. . 19 | 20 | clean-github: 21 | @rm -rf *.html .buildinfo *.inv *.js _sources 22 | 23 | # Catch-all target: route all unknown targets to Sphinx using the new 24 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 25 | %: Makefile 26 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 27 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on `Keep a 7 | Changelog <http://keepachangelog.com/en/1.0.0/>`__ and this project 8 | adheres to `Semantic Versioning <http://semver.org/spec/v2.0.0.html>`__. 9 | 10 | `v0.2.2 <https://github.com/tj-django/django-model-subscription/releases/tag/v0.2.2>`__ - 2021-09-19 11 | ---------------------------------------------------------------------------------------------------- 12 | 13 | \ `Compare with 14 | v0.2.1 <https://github.com/tj-django/django-model-subscription/compare/v0.2.1...v0.2.2>`__\ 15 | 16 | Added 17 | ~~~~~ 18 | 19 | - Added .pre-commit-config.yaml 20 | (`16061eb <https://github.com/tj-django/django-model-subscription/commit/16061ebef0760f32a97bbf9fbba7e31e074a5ac2>`__ 21 | by Tonye Jack). 22 | - Added .github/workflows/codacy-analysis.yml 23 | (`726def6 <https://github.com/tj-django/django-model-subscription/commit/726def6b516c2449fa89919df4e1c501df64c6e2>`__ 24 | by Tonye Jack). 25 | - Added support for github actions image (#221) 26 | (`e3cb443 <https://github.com/tj-django/django-model-subscription/commit/e3cb4431644f5bf6dbd733d71681539b2dd7ca70>`__ 27 | by Tonye Jack). 28 | - Added .github/issue_template/bug_report.yaml 29 | (`b8f53c6 <https://github.com/tj-django/django-model-subscription/commit/b8f53c66aac77326e91ac1a3818077492e2d097c>`__ 30 | by Tonye Jack). 31 | - Added code_of_conduct.md 32 | (`e520555 <https://github.com/tj-django/django-model-subscription/commit/e5205554b3dca0900d1350a5fe5162e4b1fc872a>`__ 33 | by Tonye Jack). 34 | - Added .github/issue_template/feature_request.yaml 35 | (`c59fbd7 <https://github.com/tj-django/django-model-subscription/commit/c59fbd7c36bd922005d1f14f8d1fe1e676b55fae>`__ 36 | by Tonye Jack). 37 | - Added .pep8speaks.yml 38 | (`8791127 <https://github.com/tj-django/django-model-subscription/commit/8791127663b79c6d804bcaf2713bd4e7443bbf7b>`__ 39 | by Tonye Jack). 40 | - Added .whitesource 41 | (`c98428f <https://github.com/tj-django/django-model-subscription/commit/c98428f99500edfea6bbf0149cab2bf295683674>`__ 42 | by Tonye Jack). 43 | - Added .github/workflow/auto-merge.yml 44 | (`ffa3288 <https://github.com/tj-django/django-model-subscription/commit/ffa328834ec4deebd66e746516cb64e07127a504>`__ 45 | by Tonye Jack). 46 | - Added .github/workflow/auto-approve.yml 47 | (`a4a558a <https://github.com/tj-django/django-model-subscription/commit/a4a558ab10cbbcdf9cfca5070d64d4f3476c0b4e>`__ 48 | by Tonye Jack). 49 | - Added .github/workspace/automerge.yml 50 | (`8ad6b30 <https://github.com/tj-django/django-model-subscription/commit/8ad6b3049744c8875f6d548a6c8d4e46e6d50edf>`__ 51 | by Tonye Jack). 52 | - Added .github/automerge.yml 53 | (`d700ccf <https://github.com/tj-django/django-model-subscription/commit/d700ccf9cb52baeca7c585266abdb215f6769aa6>`__ 54 | by Tonye Jack). 55 | - Added pre-commit-config.yaml 56 | (`309900b <https://github.com/tj-django/django-model-subscription/commit/309900b777aff080014f039f90f1ce7a543b1fd8>`__ 57 | by Tonye Jack). 58 | - Added .editorconfig 59 | (`3ed1f9b <https://github.com/tj-django/django-model-subscription/commit/3ed1f9b3efbe61429796acafb51c0d281b05a826>`__ 60 | by Tonye Jack). 61 | - Added .github/workflows/release.yml 62 | (`9af60a6 <https://github.com/tj-django/django-model-subscription/commit/9af60a6ccfad50479d78b675968c6959ac43d9ce>`__ 63 | by Tonye Jack). 64 | - Added .github/workflows/sync-release-version.yml 65 | (`6632118 <https://github.com/tj-django/django-model-subscription/commit/6632118148606c416f5219272c592730d288f838>`__ 66 | by Tonye Jack). 67 | - Added .github/issue_template/bug_report.md 68 | (`1930696 <https://github.com/tj-django/django-model-subscription/commit/193069696e08d52f9adcb63a49cbfc2182d6a467>`__ 69 | by Tonye Jack). 70 | - Added .github/issue_template/feature_request.md 71 | (`ab2723d <https://github.com/tj-django/django-model-subscription/commit/ab2723dc93197ab4ef247b9ddf662b7e0b62738f>`__ 72 | by Tonye Jack). 73 | - Added .github/auto-approve.yml 74 | (`e1c0528 <https://github.com/tj-django/django-model-subscription/commit/e1c05286240e6a68561f067cdc95b39c4d309ed5>`__ 75 | by Tonye Jack). 76 | - Added .github/workflows/auto-approve.yml 77 | (`d8d1dd5 <https://github.com/tj-django/django-model-subscription/commit/d8d1dd59f0f4f7f2db83fbf94075d9f9ceacb6cc>`__ 78 | by Tonye Jack). 79 | 80 | Fixed 81 | ~~~~~ 82 | 83 | - Fix: requirements-dev.txt to reduce vulnerabilities (#247) 84 | (`7b76394 <https://github.com/tj-django/django-model-subscription/commit/7b76394702559831bf92f662c0e5afdcfdf73002>`__ 85 | by Snyk bot). 86 | 87 | `v0.2.1 <https://github.com/tj-django/django-model-subscription/releases/tag/v0.2.1>`__ - 2021-03-21 88 | ---------------------------------------------------------------------------------------------------- 89 | 90 | \ `Compare with 91 | v0.2.0 <https://github.com/tj-django/django-model-subscription/compare/v0.2.0...v0.2.1>`__\ 92 | 93 | .. _added-1: 94 | 95 | Added 96 | ~~~~~ 97 | 98 | - Added support for coverage reporting. (#86) 99 | (`62b2a58 <https://github.com/tj-django/django-model-subscription/commit/62b2a5812a0e1626d56054885ef754a7b1800f2e>`__ 100 | by Tonye Jack). 101 | 102 | .. _fixed-1: 103 | 104 | Fixed 105 | ~~~~~ 106 | 107 | - Fixed docs. 108 | (`d85f4ba <https://github.com/tj-django/django-model-subscription/commit/d85f4ba6b998736971fa56830dbb69462a5353a3>`__ 109 | by Tonye Jack). 110 | 111 | `v0.2.0 <https://github.com/tj-django/django-model-subscription/releases/tag/v0.2.0>`__ - 2021-03-03 112 | ---------------------------------------------------------------------------------------------------- 113 | 114 | \ `Compare with 115 | v0.1.0 <https://github.com/tj-django/django-model-subscription/compare/v0.1.0...v0.2.0>`__\ 116 | 117 | .. _fixed-2: 118 | 119 | Fixed 120 | ~~~~~ 121 | 122 | - Fixed installing psycopg2 on mac. 123 | (`16a487a <https://github.com/tj-django/django-model-subscription/commit/16a487af178399df5f4b04bfe15c82249bc622ac>`__ 124 | by Tonye Jack). 125 | 126 | `v0.1.0 <https://github.com/tj-django/django-model-subscription/releases/tag/v0.1.0>`__ - 2021-02-28 127 | ---------------------------------------------------------------------------------------------------- 128 | 129 | \ `Compare with 130 | v0.0.10 <https://github.com/tj-django/django-model-subscription/compare/v0.0.10...v0.1.0>`__\ 131 | 132 | .. _fixed-3: 133 | 134 | Fixed 135 | ~~~~~ 136 | 137 | - Fixed release. 138 | (`e832ae7 <https://github.com/tj-django/django-model-subscription/commit/e832ae7159310ede9188deb70c299d2fa648e3ec>`__ 139 | by Tonye Jack). 140 | - Fixed install step. (#61) 141 | (`3c2810b <https://github.com/tj-django/django-model-subscription/commit/3c2810b2e0c4cc9c03e4dab45af1b7d9cef2d37b>`__ 142 | by Tonye Jack). 143 | - Fixed error generating docs. (#57) 144 | (`3bfa8ba <https://github.com/tj-django/django-model-subscription/commit/3bfa8babf87c305da23fc1ffc956ef31b788f6d3>`__ 145 | by Tonye Jack). 146 | - Fixed test 147 | (`04405cc <https://github.com/tj-django/django-model-subscription/commit/04405cc51cde8dcfd9eff893d473fab4eeb76a29>`__ 148 | by Tonye Jack). 149 | - Fixed test. 150 | (`05a404a <https://github.com/tj-django/django-model-subscription/commit/05a404a4ccf99e7968bdf062947b196446ff0db9>`__ 151 | by Tonye Jack). 152 | - Fixed type errors. 153 | (`8e71864 <https://github.com/tj-django/django-model-subscription/commit/8e718648bd4da553234952fee21e71dd5701a72a>`__ 154 | by Tonye Jack). 155 | - Fixed bug with docs. 156 | (`db6d7ab <https://github.com/tj-django/django-model-subscription/commit/db6d7ab584975ffdcd227fd75e1e7f14c78634fa>`__ 157 | by Tonye Jack). 158 | - Fixed flake8 errors. 159 | (`07a4419 <https://github.com/tj-django/django-model-subscription/commit/07a441925b37dc8580c968d92e5fc29de2ff2213>`__ 160 | by Tonye Jack). 161 | 162 | Removed 163 | ~~~~~~~ 164 | 165 | - Removed unused readme.rst. (#67) 166 | (`8564f0e <https://github.com/tj-django/django-model-subscription/commit/8564f0ef1b17c0b30196dbc4760f884e5b23090a>`__ 167 | by Tonye Jack). 168 | - Removed unused code. 169 | (`896e520 <https://github.com/tj-django/django-model-subscription/commit/896e520a09a049225080c4d77f2dd90d3fa16d60>`__ 170 | by Tonye Jack). 171 | - Removed unused imports. 172 | (`dceea6c <https://github.com/tj-django/django-model-subscription/commit/dceea6ca50f0ccbc29a2e608dfb63584feb5b308>`__ 173 | by Tonye Jack). 174 | 175 | `v0.0.10 <https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.10>`__ - 2020-02-28 176 | ------------------------------------------------------------------------------------------------------ 177 | 178 | \ `Compare with 179 | v0.0.9 <https://github.com/tj-django/django-model-subscription/compare/v0.0.9...v0.0.10>`__\ 180 | 181 | .. _fixed-4: 182 | 183 | Fixed 184 | ~~~~~ 185 | 186 | - Fixed missing pg_config. 187 | (`bcffacc <https://github.com/tj-django/django-model-subscription/commit/bcffacc49983439e390bfe8bd9896122569f6dfb>`__ 188 | by Tonye Jack). 189 | - Fixed permission error. 190 | (`9aec463 <https://github.com/tj-django/django-model-subscription/commit/9aec463a803a8ca099e10377d10619f6dcdae461>`__ 191 | by Tonye Jack). 192 | - Fixed workflow. 193 | (`c157de1 <https://github.com/tj-django/django-model-subscription/commit/c157de1e6721d81eef00bb208fa0df3751767d00>`__ 194 | by Tonye Jack). 195 | 196 | .. _removed-1: 197 | 198 | Removed 199 | ~~~~~~~ 200 | 201 | - Removed unused import. 202 | (`3b3a952 <https://github.com/tj-django/django-model-subscription/commit/3b3a9522c26d79bea4d8d819e171052ae6c07863>`__ 203 | by Tonye Jack). 204 | 205 | `v0.0.9 <https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.9>`__ - 2019-11-19 206 | ---------------------------------------------------------------------------------------------------- 207 | 208 | \ `Compare with 209 | v0.0.6 <https://github.com/tj-django/django-model-subscription/compare/v0.0.6...v0.0.9>`__\ 210 | 211 | .. _fixed-5: 212 | 213 | Fixed 214 | ~~~~~ 215 | 216 | - Fixed link. 217 | (`60e75d5 <https://github.com/tj-django/django-model-subscription/commit/60e75d5c49da134dabc187a1ad16468d3fc159d9>`__ 218 | by Tonye Jack). 219 | 220 | `v0.0.6 <https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.6>`__ - 2019-11-04 221 | ---------------------------------------------------------------------------------------------------- 222 | 223 | \ `Compare with 224 | v0.0.5 <https://github.com/tj-django/django-model-subscription/compare/v0.0.5...v0.0.6>`__\ 225 | 226 | .. _added-2: 227 | 228 | Added 229 | ~~~~~ 230 | 231 | - Added env vars. 232 | (`5738df6 <https://github.com/tj-django/django-model-subscription/commit/5738df696b061f4c7343e63d2bf4508090d26ef5>`__ 233 | by Tonye Jack). 234 | - Added documentation and improved readme.md 235 | (`946eac6 <https://github.com/tj-django/django-model-subscription/commit/946eac64bd4505fe6bd02da8eef6febb852c9ab4>`__ 236 | by Tonye Jack). 237 | 238 | .. _fixed-6: 239 | 240 | Fixed 241 | ~~~~~ 242 | 243 | - Fixed connection errors. 244 | (`cc048ad <https://github.com/tj-django/django-model-subscription/commit/cc048ad111f9c53e57612b98027e217f68acb80d>`__ 245 | by Tonye Jack). 246 | - Fixed settings. 247 | (`09c26e0 <https://github.com/tj-django/django-model-subscription/commit/09c26e02a552163fd03e8749aa08610244f565b5>`__ 248 | by Tonye Jack). 249 | - Fixed test command. 250 | (`ccb0d7a <https://github.com/tj-django/django-model-subscription/commit/ccb0d7a7c731755c3c0f9614a63015619befa9a8>`__ 251 | by Tonye Jack). 252 | 253 | .. _removed-2: 254 | 255 | Removed 256 | ~~~~~~~ 257 | 258 | - Removed sdist. 259 | (`e2dcdae <https://github.com/tj-django/django-model-subscription/commit/e2dcdaec4a60e3ebdfc76c21ae479900a2e5652d>`__ 260 | by Tonye Jack). 261 | 262 | `v0.0.5 <https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.5>`__ - 2019-10-23 263 | ---------------------------------------------------------------------------------------------------- 264 | 265 | \ `Compare with 266 | v0.0.4 <https://github.com/tj-django/django-model-subscription/compare/v0.0.4...v0.0.5>`__\ 267 | 268 | .. _fixed-7: 269 | 270 | Fixed 271 | ~~~~~ 272 | 273 | - Fixed bug with modelsubscription and added new decorators. 274 | (`6c616ed <https://github.com/tj-django/django-model-subscription/commit/6c616edb8f27b3287b22289657d8e758d684f815>`__ 275 | by Tonye Jack). 276 | 277 | `v0.0.4 <https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.4>`__ - 2019-10-21 278 | ---------------------------------------------------------------------------------------------------- 279 | 280 | \ `Compare with 281 | v0.0.3 <https://github.com/tj-django/django-model-subscription/compare/v0.0.3...v0.0.4>`__\ 282 | 283 | .. _added-3: 284 | 285 | Added 286 | ~~~~~ 287 | 288 | - Added model subscription to sdist. 289 | (`df9074e <https://github.com/tj-django/django-model-subscription/commit/df9074e715d894632b1dd97d4b59f7a13a05e622>`__ 290 | by Tonye Jack). 291 | 292 | .. _removed-3: 293 | 294 | Removed 295 | ~~~~~~~ 296 | 297 | - Removed unrelated settings. 298 | (`f270a93 <https://github.com/tj-django/django-model-subscription/commit/f270a9360beaec4147bf107808a45b720b1c6d34>`__ 299 | by Tonye Jack). 300 | 301 | `v0.0.3 <https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.3>`__ - 2019-10-21 302 | ---------------------------------------------------------------------------------------------------- 303 | 304 | \ `Compare with 305 | v0.0.1 <https://github.com/tj-django/django-model-subscription/compare/v0.0.1...v0.0.3>`__\ 306 | 307 | .. _removed-4: 308 | 309 | Removed 310 | ~~~~~~~ 311 | 312 | - Removed .bumpversion.cfg 313 | (`2140477 <https://github.com/tj-django/django-model-subscription/commit/2140477779916f3c7f1abc003116c6371a6ff8d0>`__ 314 | by Tonye Jack). 315 | 316 | `v0.0.1 <https://github.com/tj-django/django-model-subscription/releases/tag/v0.0.1>`__ - 2019-10-20 317 | ---------------------------------------------------------------------------------------------------- 318 | 319 | \ `Compare with first 320 | commit <https://github.com/tj-django/django-model-subscription/compare/fadbc19ce2b1307403e85a707d085e865bcfe453...v0.0.1>`__\ 321 | 322 | .. _added-4: 323 | 324 | Added 325 | ~~~~~ 326 | 327 | - Added subscription and observers module. 328 | (`4405e25 <https://github.com/tj-django/django-model-subscription/commit/4405e25da8b90d77aa0c4fe306836d5c4b7f7e41>`__ 329 | by Tonye Jack). 330 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | 18 | import django 19 | from recommonmark.parser import CommonMarkParser 20 | 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | 24 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 25 | 26 | os.environ["DJANGO_SETTINGS_MODULE"] = "django_model_subscription.settings" 27 | django.setup() 28 | 29 | source_parsers = { 30 | ".md": CommonMarkParser, 31 | } 32 | 33 | # -- Project information ----------------------------------------------------- 34 | 35 | project = "Django Model Subscription" 36 | copyright = "2019, Tonye Jack" 37 | author = "Tonye Jack" 38 | 39 | # The short X.Y version 40 | version = "" 41 | # The full version, including alpha/beta/rc tags 42 | release = "0.0.5" 43 | 44 | 45 | # -- General configuration --------------------------------------------------- 46 | 47 | # If your documentation needs a minimal Sphinx version, state it here. 48 | # 49 | # needs_sphinx = '1.0' 50 | 51 | # Add any Sphinx extension module names here, as strings. They can be 52 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 53 | # ones. 54 | extensions = [ 55 | "sphinx.ext.autodoc", 56 | "sphinx.ext.doctest", 57 | ] 58 | 59 | # Add any paths that contain templates here, relative to this directory. 60 | templates_path = ["_templates"] 61 | 62 | # The suffix(es) of source filenames. 63 | # You can specify multiple suffix as a list of string: 64 | # 65 | # source_suffix = ['.rst', '.md'] 66 | source_suffix = [".rst", ".md"] 67 | 68 | # The master toctree document. 69 | master_doc = "index" 70 | 71 | # The language for content autogenerated by Sphinx. Refer to documentation 72 | # for a list of supported languages. 73 | # 74 | # This is also used if you do content translation via gettext catalogs. 75 | # Usually you set "language" from the command line for these cases. 76 | language = None 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | # This pattern also affects html_static_path and html_extra_path. 81 | exclude_patterns = [] 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = "sphinx" 85 | 86 | 87 | # -- Options for HTML output ------------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | # 92 | html_theme = "sphinx_rtd_theme" 93 | html_theme_options = { 94 | "display_version": True, 95 | "prev_next_buttons_location": "top", 96 | "style_external_links": True, 97 | # Toc options 98 | "collapse_navigation": True, 99 | "sticky_navigation": True, 100 | "navigation_depth": 5, 101 | "includehidden": False, 102 | "titles_only": False, 103 | } 104 | 105 | # Theme options are theme-specific and customize the look and feel of a theme 106 | # further. For a list of options available for each theme, see the 107 | # documentation. 108 | # 109 | # html_theme_options = {} 110 | 111 | # Add any paths that contain custom static files (such as style sheets) here, 112 | # relative to this directory. They are copied after the builtin static files, 113 | # so a file named "default.css" will overwrite the builtin "default.css". 114 | html_static_path = [] 115 | 116 | # Custom sidebar templates, must be a dictionary that maps document names 117 | # to template names. 118 | # 119 | # The default sidebars (for documents that don't match any pattern) are 120 | # defined by theme itself. Builtin themes are using these templates by 121 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 122 | # 'searchbox.html']``. 123 | # 124 | # html_sidebars = {} 125 | 126 | 127 | # -- Options for HTMLHelp output --------------------------------------------- 128 | 129 | # Output file base name for HTML help builder. 130 | htmlhelp_basename = "DjangoModelSubscriptiondoc" 131 | 132 | 133 | # -- Options for LaTeX output ------------------------------------------------ 134 | 135 | latex_elements = { 136 | # The paper size ('letterpaper' or 'a4paper'). 137 | # 138 | # 'papersize': 'letterpaper', 139 | # The font size ('10pt', '11pt' or '12pt'). 140 | # 141 | # 'pointsize': '10pt', 142 | # Additional stuff for the LaTeX preamble. 143 | # 144 | # 'preamble': '', 145 | # Latex figure (float) alignment 146 | # 147 | # 'figure_align': 'htbp', 148 | } 149 | 150 | # Grouping the document tree into LaTeX files. List of tuples 151 | # (source start file, target name, title, 152 | # author, documentclass [howto, manual, or own class]). 153 | latex_documents = [ 154 | ( 155 | master_doc, 156 | "DjangoModelSubscription.tex", 157 | "Django Model Subscription Documentation", 158 | "Tonye Jack", 159 | "manual", 160 | ), 161 | ] 162 | 163 | 164 | # -- Options for manual page output ------------------------------------------ 165 | 166 | # One entry per manual page. List of tuples 167 | # (source start file, name, description, authors, manual section). 168 | man_pages = [ 169 | ( 170 | master_doc, 171 | "djangomodelsubscription", 172 | "Django Model Subscription Documentation", 173 | [author], 174 | 1, 175 | ) 176 | ] 177 | 178 | 179 | # -- Options for Texinfo output ---------------------------------------------- 180 | 181 | # Grouping the document tree into Texinfo files. List of tuples 182 | # (source start file, target name, title, author, 183 | # dir menu entry, description, category) 184 | texinfo_documents = [ 185 | ( 186 | master_doc, 187 | "DjangoModelSubscription", 188 | "Django Model Subscription Documentation", 189 | author, 190 | "DjangoModelSubscription", 191 | "One line description of project.", 192 | "Miscellaneous", 193 | ), 194 | ] 195 | 196 | 197 | # -- Options for Epub output ------------------------------------------------- 198 | 199 | # Bibliographic Dublin Core info. 200 | epub_title = project 201 | 202 | # The unique identifier of the text. This can be a ISBN number 203 | # or the project homepage. 204 | # 205 | # epub_identifier = '' 206 | 207 | # A unique identification for the text. 208 | # 209 | # epub_uid = '' 210 | 211 | # A list of files that should not be packed into the epub file. 212 | epub_exclude_files = ["search.html"] 213 | 214 | 215 | # -- Extension configuration ------------------------------------------------- 216 | 217 | # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_mock_imports 218 | autodoc_mock_imports = ["mysqlclient"] 219 | -------------------------------------------------------------------------------- /docs/source/configuration.rst: -------------------------------------------------------------------------------- 1 | ************* 2 | Configuration 3 | ************* 4 | 5 | Add ``model_subscription`` to your ``INSTALLED_APPS``. 6 | 7 | 8 | .. code-block:: python 9 | 10 | INSTALLED_APPS = [ 11 | ... 12 | 'model_subscription', 13 | ... 14 | ] 15 | -------------------------------------------------------------------------------- /docs/source/contents.rst: -------------------------------------------------------------------------------- 1 | **************** 2 | Package contents 3 | **************** 4 | 5 | django-model-subscription 6 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 7 | 8 | .. toctree:: 9 | :maxdepth: 5 10 | 11 | model_subscription 12 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Django Model Subscription documentation master file, created by 2 | sphinx-quickstart on Sun Oct 27 17:46:39 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Django Model Subscription's documentation! 7 | ===================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Package Documentation: 12 | 13 | installation 14 | configuration 15 | settings 16 | register_subscribers 17 | subscription_model 18 | subscribers 19 | contents 20 | changelog 21 | 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Installation 3 | ************ 4 | 5 | Via Python Package (Pip) 6 | ======================== 7 | 8 | .. code-block:: bash 9 | 10 | pip install django_model_subscription 11 | 12 | 13 | Via Poetry 14 | ========== 15 | 16 | .. code-block:: bash 17 | 18 | poetry add django_model_subscription 19 | 20 | Via setup.py 21 | ============ 22 | 23 | Add to the project ``setup.py``. 24 | 25 | .. code-block:: python 26 | 27 | setup( 28 | ... 29 | install_requires=['django_model_subscription'], 30 | ... 31 | ) 32 | -------------------------------------------------------------------------------- /docs/source/model_subscription.rst: -------------------------------------------------------------------------------- 1 | model_subscription.constants module 2 | ------------------------------------ 3 | 4 | .. automodule:: model_subscription.constants 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | model_subscription.decorators module 10 | ------------------------------------------ 11 | 12 | .. automodule:: model_subscription.decorators 13 | :members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | model_subscription.mixin module 18 | ------------------------------------------ 19 | 20 | .. automodule:: model_subscription.mixin 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | 25 | model_subscription.models module 26 | ------------------------------------------ 27 | 28 | .. automodule:: model_subscription.models 29 | :members: 30 | :undoc-members: 31 | :show-inheritance: 32 | 33 | model_subscription.observers module 34 | ------------------------------------------ 35 | 36 | .. automodule:: model_subscription.observers 37 | :members: 38 | :undoc-members: 39 | :show-inheritance: 40 | 41 | model_subscription.subscriber module 42 | ------------------------------------------ 43 | 44 | .. automodule:: model_subscription.subscriber 45 | :members: 46 | :undoc-members: 47 | :show-inheritance: 48 | -------------------------------------------------------------------------------- /docs/source/register_subscribers.rst: -------------------------------------------------------------------------------- 1 | Register subscribers 2 | ==================== 3 | 4 | Most use case for subscribing to model events are specific to when the app is running similar to how 5 | django signals work. 6 | 7 | Using App.ready (Recommended) 8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | Use the apps.ready for more control over which subscribers needs to be registered at runtime. 10 | 11 | .. warning:: Ensure ``SUBSCRIPTION_AUTO_DISCOVER`` is set to False or omitted from the settings. 12 | 13 | - Edit the `__init__.py` in your app setting the `default_app_config`. 14 | 15 | 16 | .. code-block:: python 17 | 18 | default_app_config = 'my_app.apps.MyAppConfig' 19 | 20 | 21 | - Edit `apps.py` module importing you subscription module. 22 | 23 | .. code-block:: python 24 | 25 | from django.apps import AppConfig 26 | 27 | class MyAppConfig(AppConfig): 28 | name = 'my_app' 29 | 30 | def ready(self): 31 | from my_app import subscription 32 | 33 | 34 | 35 | Using Auto Discovery 36 | ~~~~~~~~~~~~~~~~~~~~ 37 | 38 | Sample Settings 39 | ---------------- 40 | 41 | .. code-block:: python 42 | 43 | SUBSCRIPTION_MODULE = 'subscription' # This requires an app_name.subscription module/package 44 | SUBSCRIPTION_AUTO_DISCOVER = True # Turns on auto discovery 45 | -------------------------------------------------------------------------------- /docs/source/settings.rst: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | ``SUBSCRIPTION_MODULE``: Set the module or package name where auto-discovery should look for 5 | application level subscriptions (Defaults to ``subscription``). 6 | 7 | .. warning:: Ensure that ``subscription`` module is a submodule of your app. 8 | 9 | ``SUBSCRIPTION_AUTO_DISCOVER``: Toggle Auto discovery on/off (Defaults to ``False``). 10 | 11 | .. warning:: With auto discovery on this would trigger subscriptions anywhere the model object is used 12 | from scripts to executing management commands. 13 | 14 | ``NOTIFY_BULK_CREATE_SUBSCRIBERS_WITHOUT_PKS``: Notify Bulk create subscribers when the database 15 | doesn't support returning id's for bulk inserts. (Defaults: ``connection.features.can_return_ids_from_bulk_insert``) 16 | 17 | 18 | .. warning:: Note if set to ``True``, this would return the same objects passed to bulk create so 19 | accessing the ``pk`` or ``id`` field would return ``None``, if your database backend doesn't support 20 | returning id's for bulk inserts. 21 | -------------------------------------------------------------------------------- /docs/source/subscribers.rst: -------------------------------------------------------------------------------- 1 | Writing a Subscriber 2 | ==================== 3 | -------------------------------------------------------------------------------- /docs/source/subscription_model.rst: -------------------------------------------------------------------------------- 1 | Subscriber Model 2 | ================ 3 | 4 | To enable observers that listen to model changes. 5 | 6 | Update your django model by subclaassing SubscriptionModel 7 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 | 9 | * Swap ``models.Model`` with ``model_subscription.SubscriptionModel`` 10 | * This comes with it's own ``objects`` QuerySet manager. 11 | 12 | 13 | **BEFORE** 14 | 15 | .. code-block:: python 16 | 17 | from django.db import models 18 | 19 | 20 | class MyModel(models.Model): 21 | field_a = models.CharField(max_length=255) 22 | 23 | 24 | **AFTER** 25 | 26 | .. code-block:: python 27 | 28 | from moddel_subscripton import SubscriptionModel 29 | 30 | 31 | class MyModel(SubscriptionModel): 32 | field_a = models.CharField(max_length=255) 33 | 34 | 35 | Alternatively if you don't want to subclass the ``SubscriptionModel`` see below. 36 | 37 | Using the ``SubscriptionModelMixin`` and the ``SubscriptionQuerySet`` 38 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 39 | 40 | **BEFORE** 41 | 42 | .. code-block:: python 43 | 44 | from django.db import models 45 | 46 | 47 | class MyModelQuerySet(models.QuerySet): 48 | 49 | def active(self): 50 | ... 51 | 52 | 53 | class MyModel(models.Model): 54 | field_a = models.CharField(max_length=255) 55 | 56 | objects = MyModelQuerySet.as_manager() 57 | 58 | 59 | **AFTER** 60 | 61 | 62 | .. code-block:: python 63 | 64 | from django.db import models 65 | 66 | from model_subscription.mixin import SubscriptionModelMixin 67 | from model_subscription.model import SubscriptionQuerySet 68 | 69 | 70 | class MyModelQuerySet(SubscriptionQuerySet): 71 | 72 | def active(self): 73 | ... 74 | 75 | 76 | class MyModel(SubscriptionModelMixin, models.Model): 77 | field_a = models.CharField(max_length=255) 78 | 79 | objects = MyModelQuerySet.as_manager() 80 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault( 7 | "DJANGO_SETTINGS_MODULE", "django_model_subscription.settings" 8 | ) 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | -------------------------------------------------------------------------------- /model_subscription/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["constants", "decorators", "models", "mixin"] 2 | -------------------------------------------------------------------------------- /model_subscription/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ModelSubscriptionConfig(AppConfig): 5 | name = "model_subscription" 6 | -------------------------------------------------------------------------------- /model_subscription/constants.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, unique 2 | 3 | 4 | @unique 5 | class OperationType(str, Enum): 6 | """ 7 | Operation Types. 8 | """ 9 | 10 | CREATE = "create" 11 | BULK_CREATE = "bulk_create" 12 | UPDATE = "update" 13 | BULK_UPDATE = "bulk_update" 14 | DELETE = "delete" 15 | BULK_DELETE = "bulk_delete" 16 | -------------------------------------------------------------------------------- /model_subscription/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from django.conf import settings 4 | 5 | from model_subscription.constants import OperationType 6 | 7 | __all__ = [ 8 | "subscribe", 9 | "create_subscription", 10 | "bulk_create_subscription", 11 | "update_subscription", 12 | "delete_subscription", 13 | "unsubscribe", 14 | "unsubscribe_create", 15 | "unsubscribe_bulk_create", 16 | "unsubscribe_update", 17 | "unsubscribe_delete", 18 | ] 19 | 20 | 21 | """ 22 | Using the subscribe decorator. 23 | 24 | @subscribe(CREATE, TestModel) 25 | def my_custom_create_receiver(instance) 26 | pass 27 | 28 | @subscribe(BULK_CREATE, TestModel) 29 | def my_custom_bulk_create_receiver(instance) 30 | pass 31 | 32 | @subscribe(UPDATE, TestModel) 33 | def my_custom_update_receiver(instance, updated_data) 34 | pass 35 | 36 | @subscribe(DELETE, TestModel) 37 | def my_custom_delete_receiver(instance) 38 | pass 39 | 40 | 41 | # (Create, Bulk Create, Update, Delete) decorators 42 | 43 | @create_subscription(TestModel) 44 | def my_custom_create_receiver(instance) 45 | pass 46 | 47 | @bulk_create_subscription(TestModel) 48 | def my_custom_bulk_create_receiver(instances): 49 | pass 50 | 51 | @update_subscription(TestModel) 52 | def my_custom_update_receiver(instance, changed_data) 53 | pass 54 | 55 | @delete_subscription(TestModel) 56 | def my_custom_delete_receiver(instance) 57 | pass 58 | 59 | """ 60 | 61 | 62 | def subscribe(operation, model): 63 | # type: (OperationType, Type[SubscriptionModelMixin]) -> Callable[[T], None] 64 | disabled = getattr(settings, "SUBSCRIPTION_DISABLE_SUBSCRIBERS", False) 65 | 66 | if disabled: 67 | 68 | def noop(func): 69 | pass 70 | 71 | return noop 72 | 73 | def _decorator(func): 74 | model._subscription.attach(operation, func) 75 | return func 76 | 77 | return _decorator 78 | 79 | 80 | create_subscription = partial(subscribe, OperationType.CREATE) 81 | bulk_create_subscription = partial(subscribe, OperationType.BULK_CREATE) 82 | update_subscription = partial(subscribe, OperationType.UPDATE) 83 | bulk_update_subscription = partial(subscribe, OperationType.BULK_UPDATE) 84 | delete_subscription = partial(subscribe, OperationType.DELETE) 85 | bulk_delete_subscription = partial(subscribe, OperationType.BULK_DELETE) 86 | 87 | 88 | def unsubscribe(operation, model, func=None): 89 | # type: (OperationType, Type[SubscriptionModelMixin], Optional[Callable[[T], Any]]) -> Callable[[T], Any] 90 | if func is not None: 91 | model._subscription.detach(operation, func) 92 | return func 93 | 94 | def _decorator(inner): 95 | model._subscription.detach(operation, inner) 96 | return inner 97 | 98 | return _decorator 99 | 100 | 101 | unsubscribe_create = partial(unsubscribe, OperationType.CREATE) 102 | unsubscribe_bulk_create = partial(unsubscribe, OperationType.BULK_CREATE) 103 | unsubscribe_update = partial(unsubscribe, OperationType.UPDATE) 104 | unsubscribe_bulk_update = partial(unsubscribe, OperationType.BULK_UPDATE) 105 | unsubscribe_delete = partial(unsubscribe, OperationType.DELETE) 106 | unsubscribe_bulk_delete = partial(unsubscribe, OperationType.BULK_DELETE) 107 | -------------------------------------------------------------------------------- /model_subscription/mixin.py: -------------------------------------------------------------------------------- 1 | import six 2 | from django.conf import settings 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django.db.models.base import ModelBase 5 | from django_lifecycle import LifecycleModelMixin, hook 6 | 7 | from model_subscription.constants import OperationType 8 | 9 | 10 | class SubscriptionMeta(ModelBase): 11 | """ 12 | The Singleton base metaclass. 13 | """ 14 | 15 | def __new__(mcs, name, bases, attrs): 16 | from model_subscription.subscriber import ModelSubscription 17 | 18 | for base in bases: 19 | if hasattr(bases, "_subscription"): 20 | del base["_subscription"] 21 | _subscription = ModelSubscription() # type: ignore 22 | attrs["_subscription"] = _subscription 23 | return super(SubscriptionMeta, mcs).__new__(mcs, name, bases, attrs) 24 | 25 | 26 | @six.add_metaclass(SubscriptionMeta) 27 | class SubscriptionModelMixin(LifecycleModelMixin): 28 | def __init__(self, *args, **kwargs): 29 | if getattr(settings, "SUBSCRIPTION_AUTO_DISCOVER", False): 30 | if not hasattr(settings, "SUBSCRIPTION_MODULE"): 31 | raise ImproperlyConfigured( 32 | "Error no settings.SUBSCRIPTION_MODULE provided." 33 | ) 34 | self._subscription.auto_discover() 35 | super(SubscriptionModelMixin, self).__init__(*args, **kwargs) 36 | 37 | @classmethod 38 | def notify_bulk_create(cls, objs): 39 | cls._subscription.notify_many(OperationType.BULK_CREATE, objs) 40 | 41 | @classmethod 42 | def notify_bulk_update(cls, objs): 43 | cls._subscription.notify_many(OperationType.BULK_UPDATE, objs) 44 | 45 | @classmethod 46 | def notify_bulk_delete(cls, objs): 47 | cls._subscription.notify_many(OperationType.BULK_DELETE, objs) 48 | 49 | @hook("after_create") 50 | def notify_create(self): 51 | self._subscription.notify(OperationType.CREATE, self) 52 | 53 | @hook("after_update") 54 | def notify_update(self): 55 | self._subscription.notify(OperationType.UPDATE, self) 56 | 57 | @hook("after_delete") 58 | def notify_delete(self): 59 | self._subscription.notify(OperationType.DELETE, self) 60 | -------------------------------------------------------------------------------- /model_subscription/models.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db import connections, models 3 | 4 | from model_subscription.mixin import SubscriptionModelMixin 5 | from model_subscription.utils import can_return_rows_from_bulk_insert 6 | 7 | 8 | class SubscriptionQuerySet(models.QuerySet): # type: ignore 9 | def bulk_create(self, *args, **kwargs): 10 | objs = super(SubscriptionQuerySet, self).bulk_create(*args, **kwargs) 11 | connection = connections[self.db] 12 | can_notify_bulk_create_subscribers = getattr( 13 | settings, 14 | "NOTIFY_BULK_CREATE_SUBSCRIBERS_WITHOUT_PKS", 15 | can_return_rows_from_bulk_insert(connection), 16 | ) 17 | if can_notify_bulk_create_subscribers: 18 | self.model.notify_bulk_create(objs) 19 | return objs 20 | 21 | def update(self, **kwargs): 22 | rows = super(SubscriptionQuerySet, self).update(**kwargs) 23 | self.model.notify_bulk_update(self) 24 | return rows 25 | 26 | def delete(self): 27 | self.model.notify_bulk_delete(self) 28 | deleted, rows = super(SubscriptionQuerySet, self).delete() 29 | return deleted, rows 30 | 31 | 32 | class SubscriptionModel( # lgtm [py/conflicting-attributes] 33 | SubscriptionModelMixin, models.Model 34 | ): 35 | objects = SubscriptionQuerySet.as_manager() 36 | 37 | class Meta: 38 | abstract = True 39 | -------------------------------------------------------------------------------- /model_subscription/observers.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from abc import ABC, abstractmethod 3 | from typing import overload 4 | 5 | from model_subscription.constants import OperationType 6 | 7 | 8 | class Observer(ABC): 9 | """ 10 | The Observer interface declares the update method. 11 | """ 12 | 13 | def __init__(self): 14 | self.lock = threading.Lock() 15 | self._receivers = ( 16 | [] 17 | ) # type: List[Tuple[int, Callable[[models.Model, Dict], NoReturn]]] 18 | 19 | @property 20 | @abstractmethod 21 | def action(self): 22 | pass 23 | 24 | @overload 25 | def handle(self, instances): 26 | # type: (List[models.Model]) -> None 27 | pass 28 | 29 | @abstractmethod # noqa: F811 30 | def handle(self, instance, changed_data=None): 31 | # type: (models.Model, dict) -> None 32 | """ 33 | Receive update from subject. 34 | """ 35 | 36 | @property 37 | def receivers(self): 38 | return self._receivers 39 | 40 | @receivers.setter 41 | def receivers(self, other): 42 | # type: (Union[Callable, list]) -> None 43 | with self.lock: 44 | if isinstance(other, list): 45 | self._receivers = [] 46 | for receiver in other: 47 | if id(receiver) not in [x[0] for x in self._receivers]: 48 | self._receivers.append((id(receiver), receiver)) 49 | else: 50 | if id(other) not in [x[0] for x in self._receivers]: 51 | self._receivers.append((id(other), other)) 52 | 53 | 54 | """ 55 | Concrete Observers react to the operations issued by the Model they have been attached to. 56 | """ 57 | 58 | 59 | class CreateObserver(Observer): 60 | action = OperationType.CREATE 61 | 62 | def handle(self, instance, changed_data=None): 63 | # type: (models.Model, dict) -> None 64 | for _, receiver in self.receivers: 65 | receiver(instance) 66 | 67 | 68 | class BulkObserverMixin(object): 69 | def handle(self, instances): 70 | # type: (List[models.Model]) -> None 71 | for _, receiver in self.receivers: 72 | receiver(instances) 73 | 74 | 75 | class BulkCreateObserver(BulkObserverMixin, Observer): 76 | action = OperationType.BULK_CREATE 77 | 78 | 79 | class BulkUpdateObserver(BulkObserverMixin, Observer): 80 | action = OperationType.BULK_UPDATE 81 | 82 | 83 | class BulkDeleteObserver(BulkObserverMixin, Observer): 84 | action = OperationType.BULK_DELETE 85 | 86 | 87 | class UpdateObserver(Observer): 88 | action = OperationType.UPDATE 89 | 90 | def handle(self, instance, changed_data=None): 91 | # type: (models.Model, dict) -> None 92 | for _, receiver in self.receivers: 93 | receiver(instance, changed_data) 94 | 95 | 96 | class DeleteObserver(Observer): 97 | action = OperationType.DELETE 98 | 99 | def handle(self, instance, changed_data=None): 100 | # type: (models.Model, dict) -> None 101 | for _, receiver in self.receivers: 102 | receiver(instance) 103 | -------------------------------------------------------------------------------- /model_subscription/subscriber.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from django.conf import settings 4 | from django.utils.module_loading import autodiscover_modules 5 | 6 | from model_subscription.constants import OperationType 7 | from model_subscription.observers import ( 8 | BulkCreateObserver, 9 | BulkDeleteObserver, 10 | BulkUpdateObserver, 11 | CreateObserver, 12 | DeleteObserver, 13 | UpdateObserver, 14 | ) 15 | 16 | 17 | class BaseSubscription(ABC): 18 | @abstractmethod 19 | def attach(self, operation_type, receiver): 20 | # type: (OperationType, Callable) -> None 21 | """ 22 | Attach an observer. 23 | """ 24 | 25 | @abstractmethod 26 | def detach(self, operation_type, receiver): 27 | # type: (OperationType, Callable) -> None 28 | """ 29 | Detach an observer. 30 | """ 31 | 32 | @abstractmethod 33 | def notify(self, operation_type, instance): 34 | # type: (OperationType, Type[models.Model]) -> None 35 | """ 36 | Notify all observers about an event. 37 | """ 38 | 39 | @abstractmethod 40 | def notify_many(self, operation_type, objs): 41 | # type: (OperationType.BULK_CREATE, List[models.Model]) -> None 42 | """ 43 | Notify the observers of (bulk) actions. 44 | """ 45 | 46 | 47 | class ModelSubscription(BaseSubscription): 48 | """ 49 | Notifies observers when the state changes. 50 | """ 51 | 52 | def __init__(self): 53 | """ 54 | Subscription types and List of subscribers. 55 | """ 56 | self.__observers = frozenset( 57 | [ 58 | (OperationType.CREATE, CreateObserver()), 59 | (OperationType.BULK_CREATE, BulkCreateObserver()), 60 | (OperationType.UPDATE, UpdateObserver()), 61 | (OperationType.BULK_UPDATE, BulkUpdateObserver()), 62 | (OperationType.DELETE, DeleteObserver()), 63 | (OperationType.BULK_DELETE, BulkDeleteObserver()), 64 | ] 65 | ) # type: FrozenSet[Tuple[OperationType, Observer]] 66 | 67 | self.__subscription_model = None # type: Optional[models.Model] 68 | 69 | @property 70 | def observers(self): 71 | return dict(self._ModelSubscription__observers) 72 | 73 | @property 74 | def subscription_model(self): 75 | return self._ModelSubscription__subscription_model 76 | 77 | @subscription_model.setter 78 | def subscription_model(self, model): 79 | self._ModelSubscription__subscription_model = model 80 | 81 | def attach(self, operation_type, receiver): 82 | # type: (OperationType, Callable[[Any], Any]) -> None 83 | self.observers[operation_type].receivers = receiver 84 | 85 | def detach(self, operation_type, receiver): 86 | # type: (OperationType, Callable[[Any], Any]) -> None 87 | current_receivers = self.observers[operation_type].receivers 88 | self.observers[operation_type].receivers = [ 89 | r[1] for r in current_receivers if r[0] != id(receiver) 90 | ] 91 | 92 | @staticmethod 93 | def auto_discover(): 94 | autodiscover_modules(settings.SUBSCRIPTION_MODULE) 95 | 96 | def notify(self, operation_type, instance): 97 | # type: (Union[OperationType.CREATE, OperationType.UPDATE, OperationType.DELETE], Type[Any]) -> None 98 | self.subscription_model = instance 99 | observer = self.observers[operation_type] 100 | 101 | observer.handle( 102 | self.subscription_model, 103 | self.subscription_model._diff_with_initial, 104 | ) 105 | 106 | def notify_many(self, operation_type, objs): 107 | # type: (Union[OperationType.BULK_CREATE, OperationType.BULK_UPDATE, OperationType.BULK_DELETE], List[models.Model]) -> None 108 | observer = self.observers[operation_type] # type: BulkCreateObserver 109 | observer.handle(objs) 110 | -------------------------------------------------------------------------------- /model_subscription/tests.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.apps import apps 4 | from django.db import connections 5 | from django.test import TestCase, TransactionTestCase, override_settings 6 | 7 | from model_subscription.utils import can_return_rows_from_bulk_insert 8 | 9 | log = logging.getLogger("demo.subscription") 10 | 11 | 12 | @override_settings( 13 | SUBSCRIPTION_MODULE="subscription", 14 | SUBSCRIPTION_AUTO_DISCOVER=False, 15 | ) 16 | class ModelSubscriptionTestCase(TestCase): 17 | @staticmethod 18 | def get_model(model, app_name="demo"): 19 | return apps.get_model(app_name, model) 20 | 21 | @classmethod 22 | def setUpTestData(cls): 23 | cls.TestModel = cls.get_model("TestModel") 24 | 25 | def test_create_triggers_subscription(self): 26 | name = "test" 27 | with self.assertLogs(log, level=logging.DEBUG) as cm: 28 | self.TestModel.objects.create(name=name) 29 | 30 | self.assertEqual( 31 | cm.output, 32 | [ 33 | "DEBUG:demo.subscription:1. Created {}".format(name), 34 | "DEBUG:demo.subscription:3. Created {}".format(name), 35 | ], 36 | ) 37 | 38 | def test_update_triggers_subscription(self): 39 | name = "test" 40 | new_name = "New name" 41 | with self.assertLogs(log, level=logging.DEBUG) as cm: 42 | t = self.TestModel.objects.create(name=name) 43 | 44 | self.assertEqual(t.name, name) 45 | 46 | # Update 47 | t.name = new_name 48 | t.save() 49 | 50 | self.assertEqual( 51 | cm.output, 52 | [ 53 | "DEBUG:demo.subscription:1. Created {}".format(name), 54 | "DEBUG:demo.subscription:3. Created {}".format(name), 55 | "DEBUG:demo.subscription:Updated {}".format(new_name), 56 | ], 57 | ) 58 | 59 | def test_delete_triggers_subscription(self): 60 | name = "test" 61 | with self.assertLogs(log, level=logging.DEBUG) as cm: 62 | t = self.TestModel.objects.create(name=name) 63 | 64 | self.assertEqual(t.name, name) 65 | 66 | t.delete() 67 | 68 | self.assertEqual( 69 | cm.output, 70 | [ 71 | "DEBUG:demo.subscription:1. Created {}".format(name), 72 | "DEBUG:demo.subscription:3. Created {}".format(name), 73 | "DEBUG:demo.subscription:Deleted {}".format(name), 74 | ], 75 | ) 76 | 77 | 78 | class BaseSubscriptionTransactionTestCase(TransactionTestCase): 79 | @staticmethod 80 | def get_model(model, app_name="demo"): 81 | return apps.get_model(app_name, model) 82 | 83 | @classmethod 84 | def setUpClass(cls): 85 | super(BaseSubscriptionTransactionTestCase, cls).setUpClass() 86 | cls.TestModel = cls.get_model("TestModel") 87 | 88 | 89 | @override_settings( 90 | SUBSCRIPTION_MODULE="subscription", 91 | SUBSCRIPTION_AUTO_DISCOVER=False, 92 | ) 93 | class ModelSubscriptionSqliteTransactionTestCase(BaseSubscriptionTransactionTestCase): 94 | db_alias = "default" 95 | 96 | @override_settings(NOTIFY_BULK_CREATE_SUBSCRIBERS_WITHOUT_PKS=True) 97 | def test_bulk_create_triggers_subscription(self): 98 | names = ["new-{v}".format(v=i) for i in range(100)] 99 | # Bulk create 100 | with self.assertLogs(log, level=logging.DEBUG) as cm: 101 | self.TestModel.objects.using(self.db_alias).bulk_create( 102 | [self.TestModel(name=name) for name in names] 103 | ) 104 | 105 | self.assertEqual( 106 | cm.output, 107 | ["DEBUG:demo.subscription:Bulk Created {}".format(name) for name in names], 108 | ) 109 | 110 | @override_settings(NOTIFY_BULK_CREATE_SUBSCRIBERS_WITHOUT_PKS=True) 111 | def test_bulk_create_triggers_subscription_and_returns_none_as_ids(self): 112 | connection = connections[self.db_alias] 113 | names = ["new-{v}".format(v=i) for i in range(100)] 114 | # Bulk create 115 | with self.assertLogs(log, level=logging.DEBUG) as cm: 116 | objs = self.TestModel.objects.using(self.db_alias).bulk_create( 117 | [self.TestModel(name=name) for name in names] 118 | ) 119 | 120 | self.assertEqual( 121 | cm.output, 122 | ["DEBUG:demo.subscription:Bulk Created {}".format(name) for name in names], 123 | ) 124 | 125 | if can_return_rows_from_bulk_insert(connection): 126 | for obj in objs: 127 | self.assertIsNot(obj.pk, None) 128 | else: 129 | self.assertEqual( 130 | [None for _ in range(len(names))], [obj.pk for obj in objs] 131 | ) 132 | 133 | @override_settings(NOTIFY_BULK_CREATE_SUBSCRIBERS_WITHOUT_PKS=True) 134 | def test_bulk_update_triggers_subscription(self): 135 | connection = connections[self.db_alias] 136 | names = ["new-{v}".format(v=i) for i in range(100)] 137 | # Bulk create 138 | with self.assertLogs(log, level=logging.DEBUG) as cm: 139 | objs = self.TestModel.objects.using(self.db_alias).bulk_create( 140 | [self.TestModel(name=name) for name in names] 141 | ) 142 | 143 | self.assertEqual( 144 | cm.output, 145 | ["DEBUG:demo.subscription:Bulk Created {}".format(name) for name in names], 146 | ) 147 | new_name = "new" 148 | 149 | with self.assertLogs(log, level=logging.DEBUG) as cm: 150 | obj_pks = ( 151 | [obj.pk for obj in objs] 152 | if can_return_rows_from_bulk_insert(connection) 153 | else ( 154 | self.TestModel.objects.using(self.db_alias).values_list( 155 | "pk", flat=True 156 | ) 157 | ) 158 | ) 159 | 160 | self.TestModel.objects.using(self.db_alias).filter(id__in=obj_pks).update( 161 | name=new_name 162 | ) 163 | 164 | self.assertEqual( 165 | cm.output, 166 | [ 167 | "DEBUG:demo.subscription:Bulk Updated {}".format(new_name) 168 | for _ in range(len(obj_pks)) 169 | ], 170 | ) 171 | 172 | @override_settings(NOTIFY_BULK_CREATE_SUBSCRIBERS_WITHOUT_PKS=True) 173 | def test_bulk_delete_triggers_subscription(self): 174 | connection = connections[self.db_alias] 175 | names = ["new-{v}".format(v=i) for i in range(100)] 176 | # Bulk create 177 | with self.assertLogs(log, level=logging.DEBUG) as cm: 178 | objs = self.TestModel.objects.using(self.db_alias).bulk_create( 179 | [self.TestModel(name=name) for name in names] 180 | ) 181 | 182 | self.assertEqual( 183 | cm.output, 184 | ["DEBUG:demo.subscription:Bulk Created {}".format(name) for name in names], 185 | ) 186 | 187 | with self.assertLogs(log, level=logging.DEBUG) as cm: 188 | obj_pks = ( 189 | [obj.pk for obj in objs] 190 | if can_return_rows_from_bulk_insert(connection) 191 | else ( 192 | self.TestModel.objects.using(self.db_alias).values_list( 193 | "pk", flat=True 194 | ) 195 | ) 196 | ) 197 | 198 | self.TestModel.objects.using(self.db_alias).filter(id__in=obj_pks).delete() 199 | 200 | self.assertEqual( 201 | cm.output, 202 | ["DEBUG:demo.subscription:Bulk Deleted {}".format(name) for name in names], 203 | ) 204 | 205 | 206 | @override_settings( 207 | SUBSCRIPTION_MODULE="subscription", 208 | SUBSCRIPTION_AUTO_DISCOVER=False, 209 | ) 210 | class ModelSubscriptionPostgresTransactionTestCase( 211 | ModelSubscriptionSqliteTransactionTestCase 212 | ): 213 | db_alias = "postgres" 214 | databases = {"postgres"} 215 | 216 | 217 | # @override_settings( 218 | # SUBSCRIPTION_MODULE="subscription", 219 | # SUBSCRIPTION_AUTO_DISCOVER=False, 220 | # ) 221 | # class ModelSubscriptionMysqlTransactionTestCase( 222 | # ModelSubscriptionSqliteTransactionTestCase 223 | # ): 224 | # db_alias = "mysql" 225 | # databases = {"mysql"} 226 | -------------------------------------------------------------------------------- /model_subscription/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, TypeVar 2 | 3 | T = TypeVar("T", bound=Callable[..., Any]) 4 | -------------------------------------------------------------------------------- /model_subscription/utils.py: -------------------------------------------------------------------------------- 1 | def can_return_rows_from_bulk_insert(connection): 2 | return getattr( 3 | connection.features, 4 | "can_return_ids_from_bulk_insert", 5 | getattr( 6 | connection.features, 7 | "can_return_rows_from_bulk_insert", 8 | False, 9 | ), 10 | ) 11 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | # Mypy configuration: 3 | # https://mypy.readthedocs.io/en/latest/config_file.html 4 | python_version = 3.7 5 | 6 | allow_redefinition = False 7 | check_untyped_defs = True 8 | disallow_untyped_decorators = True 9 | disallow_any_explicit = False 10 | disallow_any_generics = True 11 | disallow_untyped_calls = True 12 | ignore_errors = False 13 | ignore_missing_imports = True 14 | implicit_reexport = False 15 | strict_optional = True 16 | strict_equality = True 17 | no_implicit_optional = True 18 | warn_unused_ignores = True 19 | warn_redundant_casts = True 20 | warn_unused_configs = True 21 | warn_unreachable = True 22 | warn_no_return = True 23 | 24 | ;plugins = 25 | ; mypy_django_plugin.main 26 | ; 27 | ;[mypy.plugins.django-stubs] 28 | ;django_settings_module = django_model_subscription.settings 29 | 30 | [mypy-demo.*.migrations.*] 31 | # Django migrations should not produce any errors: 32 | ignore_errors = True 33 | 34 | [mypy-model_subscription.*] 35 | # Silence errors model_subscription 36 | ignore_errors = True 37 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "alabaster" 3 | version = "0.7.12" 4 | description = "A configurable sidebar-enabled Sphinx theme" 5 | category = "dev" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "asgiref" 11 | version = "3.4.1" 12 | description = "ASGI specs, helper code, and adapters" 13 | category = "main" 14 | optional = false 15 | python-versions = ">=3.6" 16 | 17 | [package.dependencies] 18 | typing-extensions = {version = "*", markers = "python_version < \"3.8\""} 19 | 20 | [package.extras] 21 | tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] 22 | 23 | [[package]] 24 | name = "babel" 25 | version = "2.9.1" 26 | description = "Internationalization utilities" 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 30 | 31 | [package.dependencies] 32 | pytz = ">=2015.7" 33 | 34 | [[package]] 35 | name = "build" 36 | version = "0.7.0" 37 | description = "A simple, correct PEP517 package builder" 38 | category = "dev" 39 | optional = false 40 | python-versions = ">=3.6" 41 | 42 | [package.dependencies] 43 | colorama = {version = "*", markers = "os_name == \"nt\""} 44 | importlib-metadata = {version = ">=0.22", markers = "python_version < \"3.8\""} 45 | packaging = ">=19.0" 46 | pep517 = ">=0.9.1" 47 | tomli = ">=1.0.0" 48 | 49 | [package.extras] 50 | docs = ["furo (>=2020.11.19b18)", "sphinx (>=3.0,<4.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)"] 51 | test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "toml (>=0.10.0)", "wheel (>=0.36.0)"] 52 | typing = ["importlib-metadata (>=4.6.4)", "mypy (==0.910)", "typing-extensions (>=3.7.4.3)"] 53 | virtualenv = ["virtualenv (>=20.0.35)"] 54 | 55 | [[package]] 56 | name = "bump2version" 57 | version = "1.0.1" 58 | description = "Version-bump your software with a single command!" 59 | category = "dev" 60 | optional = false 61 | python-versions = ">=3.5" 62 | 63 | [[package]] 64 | name = "cachecontrol" 65 | version = "0.12.11" 66 | description = "httplib2 caching for requests" 67 | category = "dev" 68 | optional = false 69 | python-versions = ">=3.6" 70 | 71 | [package.dependencies] 72 | lockfile = {version = ">=0.9", optional = true, markers = "extra == \"filecache\""} 73 | msgpack = ">=0.5.2" 74 | requests = "*" 75 | 76 | [package.extras] 77 | filecache = ["lockfile (>=0.9)"] 78 | redis = ["redis (>=2.10.5)"] 79 | 80 | [[package]] 81 | name = "cachy" 82 | version = "0.3.0" 83 | description = "Cachy provides a simple yet effective caching library." 84 | category = "dev" 85 | optional = false 86 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 87 | 88 | [package.extras] 89 | redis = ["redis (>=3.3.6,<4.0.0)"] 90 | memcached = ["python-memcached (>=1.59,<2.0)"] 91 | msgpack = ["msgpack-python (>=0.5,<0.6)"] 92 | 93 | [[package]] 94 | name = "certifi" 95 | version = "2021.10.8" 96 | description = "Python package for providing Mozilla's CA Bundle." 97 | category = "dev" 98 | optional = false 99 | python-versions = "*" 100 | 101 | [[package]] 102 | name = "cffi" 103 | version = "1.15.0" 104 | description = "Foreign Function Interface for Python calling C code." 105 | category = "dev" 106 | optional = false 107 | python-versions = "*" 108 | 109 | [package.dependencies] 110 | pycparser = "*" 111 | 112 | [[package]] 113 | name = "charset-normalizer" 114 | version = "2.0.10" 115 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 116 | category = "dev" 117 | optional = false 118 | python-versions = ">=3.5.0" 119 | 120 | [package.extras] 121 | unicode_backport = ["unicodedata2"] 122 | 123 | [[package]] 124 | name = "check-manifest" 125 | version = "0.48" 126 | description = "Check MANIFEST.in in a Python source package for completeness" 127 | category = "dev" 128 | optional = false 129 | python-versions = ">=3.6" 130 | 131 | [package.dependencies] 132 | build = ">=0.1" 133 | tomli = "*" 134 | 135 | [package.extras] 136 | test = ["mock (>=3.0.0)", "pytest"] 137 | 138 | [[package]] 139 | name = "cleo" 140 | version = "0.8.1" 141 | description = "Cleo allows you to create beautiful and testable command-line interfaces." 142 | category = "dev" 143 | optional = false 144 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 145 | 146 | [package.dependencies] 147 | clikit = ">=0.6.0,<0.7.0" 148 | 149 | [[package]] 150 | name = "clikit" 151 | version = "0.6.2" 152 | description = "CliKit is a group of utilities to build beautiful and testable command line interfaces." 153 | category = "dev" 154 | optional = false 155 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 156 | 157 | [package.dependencies] 158 | crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} 159 | pastel = ">=0.2.0,<0.3.0" 160 | pylev = ">=1.3,<2.0" 161 | 162 | [[package]] 163 | name = "colorama" 164 | version = "0.4.4" 165 | description = "Cross-platform colored terminal text." 166 | category = "dev" 167 | optional = false 168 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 169 | 170 | [[package]] 171 | name = "commonmark" 172 | version = "0.9.1" 173 | description = "Python parser for the CommonMark Markdown spec" 174 | category = "dev" 175 | optional = false 176 | python-versions = "*" 177 | 178 | [package.extras] 179 | test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] 180 | 181 | [[package]] 182 | name = "crashtest" 183 | version = "0.3.1" 184 | description = "Manage Python errors with ease" 185 | category = "dev" 186 | optional = false 187 | python-versions = ">=3.6,<4.0" 188 | 189 | [[package]] 190 | name = "cryptography" 191 | version = "36.0.1" 192 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 193 | category = "dev" 194 | optional = false 195 | python-versions = ">=3.6" 196 | 197 | [package.dependencies] 198 | cffi = ">=1.12" 199 | 200 | [package.extras] 201 | docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] 202 | docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] 203 | pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] 204 | sdist = ["setuptools_rust (>=0.11.4)"] 205 | ssh = ["bcrypt (>=3.1.5)"] 206 | test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] 207 | 208 | [[package]] 209 | name = "distlib" 210 | version = "0.3.4" 211 | description = "Distribution utilities" 212 | category = "dev" 213 | optional = false 214 | python-versions = "*" 215 | 216 | [[package]] 217 | name = "django" 218 | version = "3.2.14" 219 | description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." 220 | category = "main" 221 | optional = false 222 | python-versions = ">=3.6" 223 | 224 | [package.dependencies] 225 | asgiref = ">=3.3.2,<4" 226 | pytz = "*" 227 | sqlparse = ">=0.2.2" 228 | 229 | [package.extras] 230 | argon2 = ["argon2-cffi (>=19.1.0)"] 231 | bcrypt = ["bcrypt"] 232 | 233 | [[package]] 234 | name = "django-lifecycle" 235 | version = "0.9.5" 236 | description = "Declarative model lifecycle hooks." 237 | category = "main" 238 | optional = false 239 | python-versions = "*" 240 | 241 | [package.dependencies] 242 | Django = ">=2.0" 243 | urlman = ">=1.2.0" 244 | 245 | [[package]] 246 | name = "django-stubs" 247 | version = "1.9.0" 248 | description = "Mypy stubs for Django" 249 | category = "dev" 250 | optional = false 251 | python-versions = ">=3.6" 252 | 253 | [package.dependencies] 254 | django = "*" 255 | django-stubs-ext = ">=0.3.0" 256 | mypy = ">=0.910" 257 | toml = "*" 258 | types-pytz = "*" 259 | types-PyYAML = "*" 260 | typing-extensions = "*" 261 | 262 | [[package]] 263 | name = "django-stubs-ext" 264 | version = "0.3.1" 265 | description = "Monkey-patching and extensions for django-stubs" 266 | category = "dev" 267 | optional = false 268 | python-versions = ">=3.6" 269 | 270 | [package.dependencies] 271 | django = "*" 272 | typing-extensions = "*" 273 | 274 | [[package]] 275 | name = "docutils" 276 | version = "0.17.1" 277 | description = "Docutils -- Python Documentation Utilities" 278 | category = "dev" 279 | optional = false 280 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 281 | 282 | [[package]] 283 | name = "filelock" 284 | version = "3.4.1" 285 | description = "A platform independent file lock." 286 | category = "dev" 287 | optional = false 288 | python-versions = ">=3.6" 289 | 290 | [package.extras] 291 | docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] 292 | testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] 293 | 294 | [[package]] 295 | name = "git-changelog" 296 | version = "0.5.0" 297 | description = "Automatic Changelog generator using Jinja2 templates." 298 | category = "dev" 299 | optional = false 300 | python-versions = ">=3.6.2" 301 | 302 | [package.dependencies] 303 | Jinja2 = ">=2.10,<4" 304 | semver = ">=2.13,<3.0" 305 | 306 | [[package]] 307 | name = "html5lib" 308 | version = "1.1" 309 | description = "HTML parser based on the WHATWG HTML specification" 310 | category = "dev" 311 | optional = false 312 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 313 | 314 | [package.dependencies] 315 | six = ">=1.9" 316 | webencodings = "*" 317 | 318 | [package.extras] 319 | all = ["genshi", "chardet (>=2.2)", "lxml"] 320 | chardet = ["chardet (>=2.2)"] 321 | genshi = ["genshi"] 322 | lxml = ["lxml"] 323 | 324 | [[package]] 325 | name = "idna" 326 | version = "3.3" 327 | description = "Internationalized Domain Names in Applications (IDNA)" 328 | category = "dev" 329 | optional = false 330 | python-versions = ">=3.5" 331 | 332 | [[package]] 333 | name = "imagesize" 334 | version = "1.3.0" 335 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 336 | category = "dev" 337 | optional = false 338 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 339 | 340 | [[package]] 341 | name = "importlib-metadata" 342 | version = "1.7.0" 343 | description = "Read metadata from Python packages" 344 | category = "dev" 345 | optional = false 346 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 347 | 348 | [package.dependencies] 349 | zipp = ">=0.5" 350 | 351 | [package.extras] 352 | docs = ["sphinx", "rst.linker"] 353 | testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] 354 | 355 | [[package]] 356 | name = "importlib-resources" 357 | version = "5.4.0" 358 | description = "Read resources from Python packages" 359 | category = "dev" 360 | optional = false 361 | python-versions = ">=3.6" 362 | 363 | [package.dependencies] 364 | zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} 365 | 366 | [package.extras] 367 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 368 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] 369 | 370 | [[package]] 371 | name = "jeepney" 372 | version = "0.7.1" 373 | description = "Low-level, pure Python DBus protocol wrapper." 374 | category = "dev" 375 | optional = false 376 | python-versions = ">=3.6" 377 | 378 | [package.extras] 379 | test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"] 380 | trio = ["trio", "async-generator"] 381 | 382 | [[package]] 383 | name = "jinja2" 384 | version = "3.0.3" 385 | description = "A very fast and expressive template engine." 386 | category = "dev" 387 | optional = false 388 | python-versions = ">=3.6" 389 | 390 | [package.dependencies] 391 | MarkupSafe = ">=2.0" 392 | 393 | [package.extras] 394 | i18n = ["Babel (>=2.7)"] 395 | 396 | [[package]] 397 | name = "keyring" 398 | version = "21.8.0" 399 | description = "Store and access your passwords safely." 400 | category = "dev" 401 | optional = false 402 | python-versions = ">=3.6" 403 | 404 | [package.dependencies] 405 | importlib-metadata = {version = ">=1", markers = "python_version < \"3.8\""} 406 | jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} 407 | pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} 408 | SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} 409 | 410 | [package.extras] 411 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 412 | testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "pytest-black (>=0.3.7)", "pytest-mypy"] 413 | 414 | [[package]] 415 | name = "livereload" 416 | version = "2.6.3" 417 | description = "Python LiveReload is an awesome tool for web developers" 418 | category = "dev" 419 | optional = false 420 | python-versions = "*" 421 | 422 | [package.dependencies] 423 | six = "*" 424 | tornado = {version = "*", markers = "python_version > \"2.7\""} 425 | 426 | [[package]] 427 | name = "lockfile" 428 | version = "0.12.2" 429 | description = "Platform-independent file locking module" 430 | category = "dev" 431 | optional = false 432 | python-versions = "*" 433 | 434 | [[package]] 435 | name = "markupsafe" 436 | version = "2.0.1" 437 | description = "Safely add untrusted strings to HTML/XML markup." 438 | category = "dev" 439 | optional = false 440 | python-versions = ">=3.6" 441 | 442 | [[package]] 443 | name = "msgpack" 444 | version = "1.0.3" 445 | description = "MessagePack (de)serializer." 446 | category = "dev" 447 | optional = false 448 | python-versions = "*" 449 | 450 | [[package]] 451 | name = "mypy" 452 | version = "0.961" 453 | description = "Optional static typing for Python" 454 | category = "dev" 455 | optional = false 456 | python-versions = ">=3.6" 457 | 458 | [package.dependencies] 459 | mypy-extensions = ">=0.4.3" 460 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 461 | typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} 462 | typing-extensions = ">=3.10" 463 | 464 | [package.extras] 465 | dmypy = ["psutil (>=4.0)"] 466 | python2 = ["typed-ast (>=1.4.0,<2)"] 467 | reports = ["lxml"] 468 | 469 | [[package]] 470 | name = "mypy-extensions" 471 | version = "0.4.3" 472 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 473 | category = "dev" 474 | optional = false 475 | python-versions = "*" 476 | 477 | [[package]] 478 | name = "mysqlclient" 479 | version = "2.1.1" 480 | description = "Python interface to MySQL" 481 | category = "dev" 482 | optional = false 483 | python-versions = ">=3.5" 484 | 485 | [[package]] 486 | name = "packaging" 487 | version = "20.9" 488 | description = "Core utilities for Python packages" 489 | category = "dev" 490 | optional = false 491 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 492 | 493 | [package.dependencies] 494 | pyparsing = ">=2.0.2" 495 | 496 | [[package]] 497 | name = "pastel" 498 | version = "0.2.1" 499 | description = "Bring colors to your terminal." 500 | category = "dev" 501 | optional = false 502 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 503 | 504 | [[package]] 505 | name = "pep517" 506 | version = "0.12.0" 507 | description = "Wrappers to build Python packages using PEP 517 hooks" 508 | category = "dev" 509 | optional = false 510 | python-versions = "*" 511 | 512 | [package.dependencies] 513 | importlib_metadata = {version = "*", markers = "python_version < \"3.8\""} 514 | tomli = {version = ">=1.1.0", markers = "python_version >= \"3.6\""} 515 | zipp = {version = "*", markers = "python_version < \"3.8\""} 516 | 517 | [[package]] 518 | name = "pexpect" 519 | version = "4.8.0" 520 | description = "Pexpect allows easy control of interactive console applications." 521 | category = "dev" 522 | optional = false 523 | python-versions = "*" 524 | 525 | [package.dependencies] 526 | ptyprocess = ">=0.5" 527 | 528 | [[package]] 529 | name = "pkginfo" 530 | version = "1.8.2" 531 | description = "Query metadatdata from sdists / bdists / installed packages." 532 | category = "dev" 533 | optional = false 534 | python-versions = "*" 535 | 536 | [package.extras] 537 | testing = ["coverage", "nose"] 538 | 539 | [[package]] 540 | name = "platformdirs" 541 | version = "2.4.0" 542 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 543 | category = "dev" 544 | optional = false 545 | python-versions = ">=3.6" 546 | 547 | [package.extras] 548 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 549 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 550 | 551 | [[package]] 552 | name = "pluggy" 553 | version = "1.0.0" 554 | description = "plugin and hook calling mechanisms for python" 555 | category = "dev" 556 | optional = false 557 | python-versions = ">=3.6" 558 | 559 | [package.dependencies] 560 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 561 | 562 | [package.extras] 563 | dev = ["pre-commit", "tox"] 564 | testing = ["pytest", "pytest-benchmark"] 565 | 566 | [[package]] 567 | name = "poetry" 568 | version = "1.1.14" 569 | description = "Python dependency management and packaging made easy." 570 | category = "dev" 571 | optional = false 572 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 573 | 574 | [package.dependencies] 575 | cachecontrol = {version = ">=0.12.9,<0.13.0", extras = ["filecache"], markers = "python_version >= \"3.6\" and python_version < \"4.0\""} 576 | cachy = ">=0.3.0,<0.4.0" 577 | cleo = ">=0.8.1,<0.9.0" 578 | clikit = ">=0.6.2,<0.7.0" 579 | crashtest = {version = ">=0.3.0,<0.4.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} 580 | html5lib = ">=1.0,<2.0" 581 | importlib-metadata = {version = ">=1.6.0,<2.0.0", markers = "python_version < \"3.8\""} 582 | keyring = {version = ">=21.2.0", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} 583 | packaging = ">=20.4,<21.0" 584 | pexpect = ">=4.7.0,<5.0.0" 585 | pkginfo = ">=1.4,<2.0" 586 | poetry-core = ">=1.0.7,<1.1.0" 587 | requests = ">=2.18,<3.0" 588 | requests-toolbelt = ">=0.9.1,<0.10.0" 589 | shellingham = ">=1.1,<2.0" 590 | tomlkit = ">=0.7.0,<1.0.0" 591 | virtualenv = ">=20.0.26,<21.0.0" 592 | 593 | [[package]] 594 | name = "poetry-core" 595 | version = "1.0.7" 596 | description = "Poetry PEP 517 Build Backend" 597 | category = "dev" 598 | optional = false 599 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 600 | 601 | [package.dependencies] 602 | importlib-metadata = {version = ">=1.7.0,<2.0.0", markers = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.5\" and python_version < \"3.8\""} 603 | 604 | [[package]] 605 | name = "psycopg2" 606 | version = "2.9.3" 607 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 608 | category = "dev" 609 | optional = false 610 | python-versions = ">=3.6" 611 | 612 | [[package]] 613 | name = "ptyprocess" 614 | version = "0.7.0" 615 | description = "Run a subprocess in a pseudo terminal" 616 | category = "dev" 617 | optional = false 618 | python-versions = "*" 619 | 620 | [[package]] 621 | name = "py" 622 | version = "1.11.0" 623 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 624 | category = "dev" 625 | optional = false 626 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 627 | 628 | [[package]] 629 | name = "pycparser" 630 | version = "2.21" 631 | description = "C parser in Python" 632 | category = "dev" 633 | optional = false 634 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 635 | 636 | [[package]] 637 | name = "pygments" 638 | version = "2.11.2" 639 | description = "Pygments is a syntax highlighting package written in Python." 640 | category = "dev" 641 | optional = false 642 | python-versions = ">=3.5" 643 | 644 | [[package]] 645 | name = "pylev" 646 | version = "1.4.0" 647 | description = "A pure Python Levenshtein implementation that's not freaking GPL'd." 648 | category = "dev" 649 | optional = false 650 | python-versions = "*" 651 | 652 | [[package]] 653 | name = "pyparsing" 654 | version = "3.0.6" 655 | description = "Python parsing module" 656 | category = "dev" 657 | optional = false 658 | python-versions = ">=3.6" 659 | 660 | [package.extras] 661 | diagrams = ["jinja2", "railroad-diagrams"] 662 | 663 | [[package]] 664 | name = "pytz" 665 | version = "2021.3" 666 | description = "World timezone definitions, modern and historical" 667 | category = "main" 668 | optional = false 669 | python-versions = "*" 670 | 671 | [[package]] 672 | name = "pywin32-ctypes" 673 | version = "0.2.0" 674 | description = "" 675 | category = "dev" 676 | optional = false 677 | python-versions = "*" 678 | 679 | [[package]] 680 | name = "recommonmark" 681 | version = "0.7.1" 682 | description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." 683 | category = "dev" 684 | optional = false 685 | python-versions = "*" 686 | 687 | [package.dependencies] 688 | commonmark = ">=0.8.1" 689 | docutils = ">=0.11" 690 | sphinx = ">=1.3.1" 691 | 692 | [[package]] 693 | name = "requests" 694 | version = "2.27.1" 695 | description = "Python HTTP for Humans." 696 | category = "dev" 697 | optional = false 698 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 699 | 700 | [package.dependencies] 701 | certifi = ">=2017.4.17" 702 | charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} 703 | idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} 704 | urllib3 = ">=1.21.1,<1.27" 705 | 706 | [package.extras] 707 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 708 | use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] 709 | 710 | [[package]] 711 | name = "requests-toolbelt" 712 | version = "0.9.1" 713 | description = "A utility belt for advanced users of python-requests" 714 | category = "dev" 715 | optional = false 716 | python-versions = "*" 717 | 718 | [package.dependencies] 719 | requests = ">=2.0.1,<3.0.0" 720 | 721 | [[package]] 722 | name = "secretstorage" 723 | version = "3.3.1" 724 | description = "Python bindings to FreeDesktop.org Secret Service API" 725 | category = "dev" 726 | optional = false 727 | python-versions = ">=3.6" 728 | 729 | [package.dependencies] 730 | cryptography = ">=2.0" 731 | jeepney = ">=0.6" 732 | 733 | [[package]] 734 | name = "semver" 735 | version = "2.13.0" 736 | description = "Python helper for Semantic Versioning (http://semver.org/)" 737 | category = "dev" 738 | optional = false 739 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 740 | 741 | [[package]] 742 | name = "shellingham" 743 | version = "1.4.0" 744 | description = "Tool to Detect Surrounding Shell" 745 | category = "dev" 746 | optional = false 747 | python-versions = "!=3.0,!=3.1,!=3.2,!=3.3,>=2.6" 748 | 749 | [[package]] 750 | name = "six" 751 | version = "1.16.0" 752 | description = "Python 2 and 3 compatibility utilities" 753 | category = "main" 754 | optional = false 755 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 756 | 757 | [[package]] 758 | name = "snowballstemmer" 759 | version = "2.2.0" 760 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 761 | category = "dev" 762 | optional = false 763 | python-versions = "*" 764 | 765 | [[package]] 766 | name = "sphinx" 767 | version = "4.3.2" 768 | description = "Python documentation generator" 769 | category = "dev" 770 | optional = false 771 | python-versions = ">=3.6" 772 | 773 | [package.dependencies] 774 | alabaster = ">=0.7,<0.8" 775 | babel = ">=1.3" 776 | colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} 777 | docutils = ">=0.14,<0.18" 778 | imagesize = "*" 779 | Jinja2 = ">=2.3" 780 | packaging = "*" 781 | Pygments = ">=2.0" 782 | requests = ">=2.5.0" 783 | snowballstemmer = ">=1.1" 784 | sphinxcontrib-applehelp = "*" 785 | sphinxcontrib-devhelp = "*" 786 | sphinxcontrib-htmlhelp = ">=2.0.0" 787 | sphinxcontrib-jsmath = "*" 788 | sphinxcontrib-qthelp = "*" 789 | sphinxcontrib-serializinghtml = ">=1.1.5" 790 | 791 | [package.extras] 792 | docs = ["sphinxcontrib-websupport"] 793 | lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.920)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"] 794 | test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] 795 | 796 | [[package]] 797 | name = "sphinx-autobuild" 798 | version = "2021.3.14" 799 | description = "Rebuild Sphinx documentation on changes, with live-reload in the browser." 800 | category = "dev" 801 | optional = false 802 | python-versions = ">=3.6" 803 | 804 | [package.dependencies] 805 | colorama = "*" 806 | livereload = "*" 807 | sphinx = "*" 808 | 809 | [package.extras] 810 | test = ["pytest", "pytest-cov"] 811 | 812 | [[package]] 813 | name = "sphinx-rtd-theme" 814 | version = "1.0.0" 815 | description = "Read the Docs theme for Sphinx" 816 | category = "dev" 817 | optional = false 818 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 819 | 820 | [package.dependencies] 821 | docutils = "<0.18" 822 | sphinx = ">=1.6" 823 | 824 | [package.extras] 825 | dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] 826 | 827 | [[package]] 828 | name = "sphinxcontrib-applehelp" 829 | version = "1.0.2" 830 | description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" 831 | category = "dev" 832 | optional = false 833 | python-versions = ">=3.5" 834 | 835 | [package.extras] 836 | lint = ["flake8", "mypy", "docutils-stubs"] 837 | test = ["pytest"] 838 | 839 | [[package]] 840 | name = "sphinxcontrib-devhelp" 841 | version = "1.0.2" 842 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." 843 | category = "dev" 844 | optional = false 845 | python-versions = ">=3.5" 846 | 847 | [package.extras] 848 | lint = ["flake8", "mypy", "docutils-stubs"] 849 | test = ["pytest"] 850 | 851 | [[package]] 852 | name = "sphinxcontrib-htmlhelp" 853 | version = "2.0.0" 854 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 855 | category = "dev" 856 | optional = false 857 | python-versions = ">=3.6" 858 | 859 | [package.extras] 860 | lint = ["flake8", "mypy", "docutils-stubs"] 861 | test = ["pytest", "html5lib"] 862 | 863 | [[package]] 864 | name = "sphinxcontrib-jsmath" 865 | version = "1.0.1" 866 | description = "A sphinx extension which renders display math in HTML via JavaScript" 867 | category = "dev" 868 | optional = false 869 | python-versions = ">=3.5" 870 | 871 | [package.extras] 872 | test = ["pytest", "flake8", "mypy"] 873 | 874 | [[package]] 875 | name = "sphinxcontrib-qthelp" 876 | version = "1.0.3" 877 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." 878 | category = "dev" 879 | optional = false 880 | python-versions = ">=3.5" 881 | 882 | [package.extras] 883 | lint = ["flake8", "mypy", "docutils-stubs"] 884 | test = ["pytest"] 885 | 886 | [[package]] 887 | name = "sphinxcontrib-serializinghtml" 888 | version = "1.1.5" 889 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." 890 | category = "dev" 891 | optional = false 892 | python-versions = ">=3.5" 893 | 894 | [package.extras] 895 | lint = ["flake8", "mypy", "docutils-stubs"] 896 | test = ["pytest"] 897 | 898 | [[package]] 899 | name = "sqlparse" 900 | version = "0.4.2" 901 | description = "A non-validating SQL parser." 902 | category = "main" 903 | optional = false 904 | python-versions = ">=3.5" 905 | 906 | [[package]] 907 | name = "toml" 908 | version = "0.10.2" 909 | description = "Python Library for Tom's Obvious, Minimal Language" 910 | category = "dev" 911 | optional = false 912 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 913 | 914 | [[package]] 915 | name = "tomli" 916 | version = "1.2.3" 917 | description = "A lil' TOML parser" 918 | category = "dev" 919 | optional = false 920 | python-versions = ">=3.6" 921 | 922 | [[package]] 923 | name = "tomlkit" 924 | version = "0.8.0" 925 | description = "Style preserving TOML library" 926 | category = "dev" 927 | optional = false 928 | python-versions = ">=3.6,<4.0" 929 | 930 | [[package]] 931 | name = "tornado" 932 | version = "6.1" 933 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 934 | category = "dev" 935 | optional = false 936 | python-versions = ">= 3.5" 937 | 938 | [[package]] 939 | name = "tox" 940 | version = "3.25.1" 941 | description = "tox is a generic virtualenv management and test command line tool" 942 | category = "dev" 943 | optional = false 944 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 945 | 946 | [package.dependencies] 947 | colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} 948 | filelock = ">=3.0.0" 949 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 950 | packaging = ">=14" 951 | pluggy = ">=0.12.0" 952 | py = ">=1.4.17" 953 | six = ">=1.14.0" 954 | toml = ">=0.9.4" 955 | virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" 956 | 957 | [package.extras] 958 | docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] 959 | testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] 960 | 961 | [[package]] 962 | name = "typed-ast" 963 | version = "1.5.1" 964 | description = "a fork of Python 2 and 3 ast modules with type comment support" 965 | category = "dev" 966 | optional = false 967 | python-versions = ">=3.6" 968 | 969 | [[package]] 970 | name = "types-pytz" 971 | version = "2021.3.4" 972 | description = "Typing stubs for pytz" 973 | category = "dev" 974 | optional = false 975 | python-versions = "*" 976 | 977 | [[package]] 978 | name = "types-pyyaml" 979 | version = "6.0.3" 980 | description = "Typing stubs for PyYAML" 981 | category = "dev" 982 | optional = false 983 | python-versions = "*" 984 | 985 | [[package]] 986 | name = "typing-extensions" 987 | version = "4.1.1" 988 | description = "Backported and Experimental Type Hints for Python 3.6+" 989 | category = "main" 990 | optional = false 991 | python-versions = ">=3.6" 992 | 993 | [[package]] 994 | name = "urllib3" 995 | version = "1.26.8" 996 | description = "HTTP library with thread-safe connection pooling, file post, and more." 997 | category = "dev" 998 | optional = false 999 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 1000 | 1001 | [package.extras] 1002 | brotli = ["brotlipy (>=0.6.0)"] 1003 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 1004 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 1005 | 1006 | [[package]] 1007 | name = "urlman" 1008 | version = "2.0.1" 1009 | description = "Django URL pattern helpers" 1010 | category = "main" 1011 | optional = false 1012 | python-versions = "*" 1013 | 1014 | [[package]] 1015 | name = "virtualenv" 1016 | version = "20.13.0" 1017 | description = "Virtual Python Environment builder" 1018 | category = "dev" 1019 | optional = false 1020 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 1021 | 1022 | [package.dependencies] 1023 | distlib = ">=0.3.1,<1" 1024 | filelock = ">=3.2,<4" 1025 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 1026 | importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} 1027 | platformdirs = ">=2,<3" 1028 | six = ">=1.9.0,<2" 1029 | 1030 | [package.extras] 1031 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] 1032 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] 1033 | 1034 | [[package]] 1035 | name = "webencodings" 1036 | version = "0.5.1" 1037 | description = "Character encoding aliases for legacy web content" 1038 | category = "dev" 1039 | optional = false 1040 | python-versions = "*" 1041 | 1042 | [[package]] 1043 | name = "zipp" 1044 | version = "3.6.0" 1045 | description = "Backport of pathlib-compatible object wrapper for zip files" 1046 | category = "dev" 1047 | optional = false 1048 | python-versions = ">=3.6" 1049 | 1050 | [package.extras] 1051 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 1052 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] 1053 | 1054 | [extras] 1055 | deploy = [] 1056 | development = [] 1057 | 1058 | [metadata] 1059 | lock-version = "1.1" 1060 | python-versions = "3.6.* || 3.7.* || 3.8.* || 3.9.*" 1061 | content-hash = "8cb4601e3b0cd214dc3b973147d1becc8bf90beaedfd8ac48a53844cfaa7820c" 1062 | 1063 | [metadata.files] 1064 | alabaster = [] 1065 | asgiref = [] 1066 | babel = [] 1067 | build = [] 1068 | bump2version = [] 1069 | cachecontrol = [] 1070 | cachy = [] 1071 | certifi = [] 1072 | cffi = [] 1073 | charset-normalizer = [] 1074 | check-manifest = [] 1075 | cleo = [] 1076 | clikit = [] 1077 | colorama = [] 1078 | commonmark = [] 1079 | crashtest = [] 1080 | cryptography = [] 1081 | distlib = [] 1082 | django = [] 1083 | django-lifecycle = [] 1084 | django-stubs = [] 1085 | django-stubs-ext = [] 1086 | docutils = [] 1087 | filelock = [] 1088 | git-changelog = [] 1089 | html5lib = [] 1090 | idna = [] 1091 | imagesize = [] 1092 | importlib-metadata = [] 1093 | importlib-resources = [] 1094 | jeepney = [] 1095 | jinja2 = [] 1096 | keyring = [] 1097 | livereload = [] 1098 | lockfile = [] 1099 | markupsafe = [] 1100 | msgpack = [] 1101 | mypy = [] 1102 | mypy-extensions = [] 1103 | mysqlclient = [] 1104 | packaging = [] 1105 | pastel = [] 1106 | pep517 = [] 1107 | pexpect = [] 1108 | pkginfo = [] 1109 | platformdirs = [] 1110 | pluggy = [] 1111 | poetry = [] 1112 | poetry-core = [] 1113 | psycopg2 = [] 1114 | ptyprocess = [] 1115 | py = [] 1116 | pycparser = [] 1117 | pygments = [] 1118 | pylev = [] 1119 | pyparsing = [] 1120 | pytz = [] 1121 | pywin32-ctypes = [] 1122 | recommonmark = [] 1123 | requests = [] 1124 | requests-toolbelt = [] 1125 | secretstorage = [] 1126 | semver = [] 1127 | shellingham = [] 1128 | six = [] 1129 | snowballstemmer = [] 1130 | sphinx = [] 1131 | sphinx-autobuild = [] 1132 | sphinx-rtd-theme = [] 1133 | sphinxcontrib-applehelp = [] 1134 | sphinxcontrib-devhelp = [] 1135 | sphinxcontrib-htmlhelp = [] 1136 | sphinxcontrib-jsmath = [] 1137 | sphinxcontrib-qthelp = [] 1138 | sphinxcontrib-serializinghtml = [] 1139 | sqlparse = [] 1140 | toml = [] 1141 | tomli = [] 1142 | tomlkit = [] 1143 | tornado = [] 1144 | tox = [] 1145 | typed-ast = [] 1146 | types-pytz = [] 1147 | types-pyyaml = [] 1148 | typing-extensions = [] 1149 | urllib3 = [] 1150 | urlman = [] 1151 | virtualenv = [] 1152 | webencodings = [] 1153 | zipp = [] 1154 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-model-subscription" 3 | version = "0.2.2" 4 | description = "Subscription model for a django model instance." 5 | authors = ["Tonye Jack <tonyejck@gmail.com>"] 6 | license = "MIT" 7 | homepage = "https://django-model-subscription.readthedocs.io/en/latest/index.html" 8 | repository = "https://github.com/jackton1/django-model-subscription" 9 | documentation = "https://django-model-subscription.readthedocs.io/en/latest/index.html" 10 | 11 | keywords = [ 12 | "django model subscription", 13 | "model observer", 14 | "model change subscriber", 15 | "model subscriptions", 16 | "model instance subscription", 17 | ] 18 | 19 | classifiers = [ 20 | 'Environment :: Web Environment', 21 | 'Development Status :: 5 - Production/Stable', 22 | 'Framework :: Django', 23 | 'Framework :: Django :: 2.0', 24 | 'Framework :: Django :: 2.1', 25 | 'Framework :: Django :: 2.2', 26 | 'Framework :: Django :: 3.0', 27 | 'Framework :: Django :: 3.1', 28 | 'Framework :: Django :: 3.2', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 3.7', 34 | 'Programming Language :: Python :: 3.8', 35 | 'Programming Language :: Python :: 3.9', 36 | 'Programming Language :: Python :: 3.10', 37 | 'Programming Language :: Python :: 3.11', 38 | 'Topic :: Internet :: WWW/HTTP', 39 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 40 | ] 41 | 42 | packages = [ 43 | { include = "model_subscription" } 44 | ] 45 | 46 | include = [ 47 | "CHANGELOG.md", 48 | ] 49 | 50 | exclude = [ 51 | "__pycache__", 52 | "*.pyc", 53 | "*.pyo", 54 | "*.orig", 55 | "tests", 56 | "model_subscription/tests.py", 57 | "demo", 58 | ] 59 | readme = "README.md" 60 | 61 | 62 | [tool.poetry.dependencies] 63 | python = "3.7.* || 3.8.* || 3.9.* || 3.10.* || 3.11.*" 64 | # The dataclasses package is not in the stdlib in Python 3.6 65 | dataclasses = { version = "^0.8", python = "~3.6" } 66 | django-lifecycle = "~1.0.0" 67 | typing_extensions = ">=3.7,<5.0" 68 | six = "^1.14" 69 | 70 | [tool.poetry.dev-dependencies] 71 | Django = {version = "^4.0.0"} 72 | check-manifest = {version = "^0.48", python = "^3.6"} 73 | bump2version = {version = "^1.0.0", python = "^3.6"} 74 | git-changelog = {version = "^1.0.0", python = "^3.6.2"} 75 | poetry = "=1.1.14" 76 | sphinx = "=4.5.0" 77 | sphinx-autobuild = "^2021.0.0" 78 | sphinx_rtd_theme = "^1.0.0" 79 | lockfile = "^0.12.2" 80 | recommonmark = "^0.7.1" 81 | psycopg2 = {version = "=2.9.6", python = "3.6.* || 3.7.* || 3.8.* || 3.9.*"} 82 | mysqlclient = {version = "=2.1.1", python = "^3.6"} 83 | tox = "^3.25" 84 | mypy = {version = "^0.961", python = "^3.6"} 85 | django-stubs = {version = "^4.0.0", python = "^3.6"} 86 | 87 | [tool.poetry.extras] 88 | deploy = ["bump2version", "git-changelog"] 89 | development = ["Django", "check-manifest"] 90 | 91 | [build-system] 92 | requires = ["poetry_core>=1.0.0"] 93 | build-backend = "poetry.core.masonry.api" 94 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "enabled": true, 6 | "prHourlyLimit": 10, 7 | "prConcurrentLimit": 5, 8 | "rebaseWhen": "behind-base-branch", 9 | "addLabels": [ 10 | "dependencies" 11 | ], 12 | "assignees": [ 13 | "jackton1" 14 | ], 15 | "assignAutomerge": true, 16 | "dependencyDashboard": true, 17 | "dependencyDashboardAutoclose": true, 18 | "lockFileMaintenance": { 19 | "enabled": true, 20 | "automerge": true 21 | }, 22 | "packageRules": [ 23 | { 24 | "matchUpdateTypes": ["major", "minor", "patch", "pin", "digest"], 25 | "automerge": true, 26 | "rebaseWhen": "behind-base-branch", 27 | "addLabels": [ 28 | "automerge" 29 | ] 30 | }, 31 | { 32 | "description": "docker images", 33 | "matchLanguages": [ 34 | "docker" 35 | ], 36 | "matchUpdateTypes": ["major", "minor", "patch", "pin", "digest"], 37 | "rebaseWhen": "behind-base-branch", 38 | "addLabels": [ 39 | "automerge" 40 | ], 41 | "automerge": true 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 180 3 | exclude = 4 | .tox, 5 | setup.py, 6 | venv, 7 | max-complexity = 10 8 | include = 9 | model_subscription, 10 | django_model_subscription, 11 | demo, 12 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.8.0 3 | envlist = 4 | yamllint 5 | flake8 6 | mypy 7 | docs 8 | py37-django{20,21,22,30,31,32,main}-{linux,macos,windows} 9 | py38-django{21,22,30,31,32,40,41,42,main}-{linux,macos,windows} 10 | py39-django{21,22,30,31,32,40,41,42,main}-{linux,macos,windows} 11 | py310-django{22,30,31,32,40,41,42,main}-{linux,macos,windows} 12 | py311-django{22,30,31,32,40,41,42,main}-{linux} 13 | skip_missing_interpreters = True 14 | isolated_build = True 15 | requires = 16 | setuptools >= 30.0.0 17 | 18 | [gh-actions] 19 | python = 20 | 3.7: py37 21 | 3.8: py38 22 | 3.9: py39 23 | 3.10: py310 24 | 3.11: py311 25 | 26 | [gh-actions:env] 27 | PLATFORM = 28 | ubuntu-latest: linux 29 | macos-latest: macos 30 | windows-latest: windows 31 | 32 | [testenv] 33 | usedevelop = true 34 | deps = 35 | django20: Django>=2.0,<2.1 36 | django21: Django>=2.1,<2.2 37 | django22: Django>=2.2,<2.3 38 | django30: Django>=3.0,<3.1 39 | django31: Django>=3.1,<3.2 40 | django32: Django>=3.2,<3.3 41 | django40: Django>=4.0,<4.1 42 | django41: Django>=4.1,<4.2 43 | django42: Django>=4.2,<4.3 44 | main: https://github.com/django/django/archive/main.tar.gz 45 | django-lifecycle==0.9.0 46 | pytz==2021.1 47 | six==1.15.0 48 | sqlparse==0.4.1 49 | typing-extensions==3.7.4.3 50 | urlman==1.4.0 51 | psycopg2==2.8.6 52 | coverage 53 | codacy-coverage 54 | mysqlclient 55 | passenv = * 56 | commands = 57 | coverage run manage.py test --no-input 58 | coverage report -m 59 | coverage xml 60 | - python-codacy-coverage -r coverage.xml 61 | 62 | [testenv:mypy] 63 | basepython = python3.7 64 | deps = mypy==0.740 65 | commands = mypy . 66 | 67 | [testenv:flake8] 68 | basepython = python3.7 69 | deps = flake8 70 | commands = flake8 . 71 | 72 | [testenv:yamllint] 73 | deps = yamllint==1.11.1 74 | commands = yamllint --strict -f standard .circleci/config.yml 75 | 76 | [testenv:docs] 77 | changedir = docs 78 | deps = 79 | -rrequirements.txt 80 | commands = sphinx-build -W -b html -d {envtmpdir}/doctrees source {envtmpdir}/html 81 | --------------------------------------------------------------------------------