├── .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] "
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]
60 | author: github-actions[bot]
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
2 | Tonye Jack
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 | [Compare with v0.2.1](https://github.com/tj-django/django-model-subscription/compare/v0.2.1...v0.2.2)
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 | [Compare with v0.2.0](https://github.com/tj-django/django-model-subscription/compare/v0.2.0...v0.2.1)
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 | [Compare with v0.1.0](https://github.com/tj-django/django-model-subscription/compare/v0.1.0...v0.2.0)
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 | [Compare with v0.0.10](https://github.com/tj-django/django-model-subscription/compare/v0.0.10...v0.1.0)
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 | [Compare with v0.0.9](https://github.com/tj-django/django-model-subscription/compare/v0.0.9...v0.0.10)
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 | [Compare with v0.0.6](https://github.com/tj-django/django-model-subscription/compare/v0.0.6...v0.0.9)
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 | [Compare with v0.0.5](https://github.com/tj-django/django-model-subscription/compare/v0.0.5...v0.0.6)
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 | [Compare with v0.0.4](https://github.com/tj-django/django-model-subscription/compare/v0.0.4...v0.0.5)
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 | [Compare with v0.0.3](https://github.com/tj-django/django-model-subscription/compare/v0.0.3...v0.0.4)
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 | [Compare with v0.0.1](https://github.com/tj-django/django-model-subscription/compare/v0.0.1...v0.0.3)
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 | [Compare with first commit](https://github.com/tj-django/django-model-subscription/compare/fadbc19ce2b1307403e85a707d085e865bcfe453...v0.0.1)
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
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 | [](https://pypi.org/project/django-model-subscription/) [](https://github.com/jackton1/django-model-subscription/actions?query=workflow%3A"django+model+subscription+test.")
2 | [](https://django-model-subscription.readthedocs.io/en/latest/?badge=latest)
3 |
4 | [](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) [](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) [](https://codecov.io/gh/tj-django/django-model-subscription) [](https://github.com/jackton1/django-model-subscription/blob/master/LICENSE)
5 | [](https://results.pre-commit.ci/latest/github/tj-django/django-model-subscription/main)
6 |
7 | [](https://pypi.org/project/django-model-subscription)
8 | [](https://docs.djangoproject.com/en/3.2/releases/)
9 | [](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 |
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 `__ and this project
8 | adheres to `Semantic Versioning `__.
9 |
10 | `v0.2.2 `__ - 2021-09-19
11 | ----------------------------------------------------------------------------------------------------
12 |
13 | \ `Compare with
14 | v0.2.1 `__\
15 |
16 | Added
17 | ~~~~~
18 |
19 | - Added .pre-commit-config.yaml
20 | (`16061eb `__
21 | by Tonye Jack).
22 | - Added .github/workflows/codacy-analysis.yml
23 | (`726def6 `__
24 | by Tonye Jack).
25 | - Added support for github actions image (#221)
26 | (`e3cb443 `__
27 | by Tonye Jack).
28 | - Added .github/issue_template/bug_report.yaml
29 | (`b8f53c6 `__
30 | by Tonye Jack).
31 | - Added code_of_conduct.md
32 | (`e520555 `__
33 | by Tonye Jack).
34 | - Added .github/issue_template/feature_request.yaml
35 | (`c59fbd7 `__
36 | by Tonye Jack).
37 | - Added .pep8speaks.yml
38 | (`8791127 `__
39 | by Tonye Jack).
40 | - Added .whitesource
41 | (`c98428f `__
42 | by Tonye Jack).
43 | - Added .github/workflow/auto-merge.yml
44 | (`ffa3288 `__
45 | by Tonye Jack).
46 | - Added .github/workflow/auto-approve.yml
47 | (`a4a558a `__
48 | by Tonye Jack).
49 | - Added .github/workspace/automerge.yml
50 | (`8ad6b30 `__
51 | by Tonye Jack).
52 | - Added .github/automerge.yml
53 | (`d700ccf `__
54 | by Tonye Jack).
55 | - Added pre-commit-config.yaml
56 | (`309900b `__
57 | by Tonye Jack).
58 | - Added .editorconfig
59 | (`3ed1f9b `__
60 | by Tonye Jack).
61 | - Added .github/workflows/release.yml
62 | (`9af60a6 `__
63 | by Tonye Jack).
64 | - Added .github/workflows/sync-release-version.yml
65 | (`6632118 `__
66 | by Tonye Jack).
67 | - Added .github/issue_template/bug_report.md
68 | (`1930696 `__
69 | by Tonye Jack).
70 | - Added .github/issue_template/feature_request.md
71 | (`ab2723d `__
72 | by Tonye Jack).
73 | - Added .github/auto-approve.yml
74 | (`e1c0528 `__
75 | by Tonye Jack).
76 | - Added .github/workflows/auto-approve.yml
77 | (`d8d1dd5 `__
78 | by Tonye Jack).
79 |
80 | Fixed
81 | ~~~~~
82 |
83 | - Fix: requirements-dev.txt to reduce vulnerabilities (#247)
84 | (`7b76394 `__
85 | by Snyk bot).
86 |
87 | `v0.2.1 `__ - 2021-03-21
88 | ----------------------------------------------------------------------------------------------------
89 |
90 | \ `Compare with
91 | v0.2.0 `__\
92 |
93 | .. _added-1:
94 |
95 | Added
96 | ~~~~~
97 |
98 | - Added support for coverage reporting. (#86)
99 | (`62b2a58 `__
100 | by Tonye Jack).
101 |
102 | .. _fixed-1:
103 |
104 | Fixed
105 | ~~~~~
106 |
107 | - Fixed docs.
108 | (`d85f4ba `__
109 | by Tonye Jack).
110 |
111 | `v0.2.0 `__ - 2021-03-03
112 | ----------------------------------------------------------------------------------------------------
113 |
114 | \ `Compare with
115 | v0.1.0 `__\
116 |
117 | .. _fixed-2:
118 |
119 | Fixed
120 | ~~~~~
121 |
122 | - Fixed installing psycopg2 on mac.
123 | (`16a487a `__
124 | by Tonye Jack).
125 |
126 | `v0.1.0 `__ - 2021-02-28
127 | ----------------------------------------------------------------------------------------------------
128 |
129 | \ `Compare with
130 | v0.0.10 `__\
131 |
132 | .. _fixed-3:
133 |
134 | Fixed
135 | ~~~~~
136 |
137 | - Fixed release.
138 | (`e832ae7 `__
139 | by Tonye Jack).
140 | - Fixed install step. (#61)
141 | (`3c2810b `__
142 | by Tonye Jack).
143 | - Fixed error generating docs. (#57)
144 | (`3bfa8ba `__
145 | by Tonye Jack).
146 | - Fixed test
147 | (`04405cc `__
148 | by Tonye Jack).
149 | - Fixed test.
150 | (`05a404a `__
151 | by Tonye Jack).
152 | - Fixed type errors.
153 | (`8e71864 `__
154 | by Tonye Jack).
155 | - Fixed bug with docs.
156 | (`db6d7ab `__
157 | by Tonye Jack).
158 | - Fixed flake8 errors.
159 | (`07a4419 `__
160 | by Tonye Jack).
161 |
162 | Removed
163 | ~~~~~~~
164 |
165 | - Removed unused readme.rst. (#67)
166 | (`8564f0e `__
167 | by Tonye Jack).
168 | - Removed unused code.
169 | (`896e520 `__
170 | by Tonye Jack).
171 | - Removed unused imports.
172 | (`dceea6c `__
173 | by Tonye Jack).
174 |
175 | `v0.0.10 `__ - 2020-02-28
176 | ------------------------------------------------------------------------------------------------------
177 |
178 | \ `Compare with
179 | v0.0.9 `__\
180 |
181 | .. _fixed-4:
182 |
183 | Fixed
184 | ~~~~~
185 |
186 | - Fixed missing pg_config.
187 | (`bcffacc `__
188 | by Tonye Jack).
189 | - Fixed permission error.
190 | (`9aec463 `__
191 | by Tonye Jack).
192 | - Fixed workflow.
193 | (`c157de1 `__
194 | by Tonye Jack).
195 |
196 | .. _removed-1:
197 |
198 | Removed
199 | ~~~~~~~
200 |
201 | - Removed unused import.
202 | (`3b3a952 `__
203 | by Tonye Jack).
204 |
205 | `v0.0.9 `__ - 2019-11-19
206 | ----------------------------------------------------------------------------------------------------
207 |
208 | \ `Compare with
209 | v0.0.6 `__\
210 |
211 | .. _fixed-5:
212 |
213 | Fixed
214 | ~~~~~
215 |
216 | - Fixed link.
217 | (`60e75d5 `__
218 | by Tonye Jack).
219 |
220 | `v0.0.6 `__ - 2019-11-04
221 | ----------------------------------------------------------------------------------------------------
222 |
223 | \ `Compare with
224 | v0.0.5 `__\
225 |
226 | .. _added-2:
227 |
228 | Added
229 | ~~~~~
230 |
231 | - Added env vars.
232 | (`5738df6 `__
233 | by Tonye Jack).
234 | - Added documentation and improved readme.md
235 | (`946eac6 `__
236 | by Tonye Jack).
237 |
238 | .. _fixed-6:
239 |
240 | Fixed
241 | ~~~~~
242 |
243 | - Fixed connection errors.
244 | (`cc048ad `__
245 | by Tonye Jack).
246 | - Fixed settings.
247 | (`09c26e0 `__
248 | by Tonye Jack).
249 | - Fixed test command.
250 | (`ccb0d7a `__
251 | by Tonye Jack).
252 |
253 | .. _removed-2:
254 |
255 | Removed
256 | ~~~~~~~
257 |
258 | - Removed sdist.
259 | (`e2dcdae `__
260 | by Tonye Jack).
261 |
262 | `v0.0.5 `__ - 2019-10-23
263 | ----------------------------------------------------------------------------------------------------
264 |
265 | \ `Compare with
266 | v0.0.4 `__\
267 |
268 | .. _fixed-7:
269 |
270 | Fixed
271 | ~~~~~
272 |
273 | - Fixed bug with modelsubscription and added new decorators.
274 | (`6c616ed `__
275 | by Tonye Jack).
276 |
277 | `v0.0.4 `__ - 2019-10-21
278 | ----------------------------------------------------------------------------------------------------
279 |
280 | \ `Compare with
281 | v0.0.3 `__\
282 |
283 | .. _added-3:
284 |
285 | Added
286 | ~~~~~
287 |
288 | - Added model subscription to sdist.
289 | (`df9074e `__
290 | by Tonye Jack).
291 |
292 | .. _removed-3:
293 |
294 | Removed
295 | ~~~~~~~
296 |
297 | - Removed unrelated settings.
298 | (`f270a93 `__
299 | by Tonye Jack).
300 |
301 | `v0.0.3 `__ - 2019-10-21
302 | ----------------------------------------------------------------------------------------------------
303 |
304 | \ `Compare with
305 | v0.0.1 `__\
306 |
307 | .. _removed-4:
308 |
309 | Removed
310 | ~~~~~~~
311 |
312 | - Removed .bumpversion.cfg
313 | (`2140477 `__
314 | by Tonye Jack).
315 |
316 | `v0.0.1 `__ - 2019-10-20
317 | ----------------------------------------------------------------------------------------------------
318 |
319 | \ `Compare with first
320 | commit `__\
321 |
322 | .. _added-4:
323 |
324 | Added
325 | ~~~~~
326 |
327 | - Added subscription and observers module.
328 | (`4405e25 `__
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 "]
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 |
--------------------------------------------------------------------------------