diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..1157ff2 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +service_name: github-actions diff --git a/.env b/.env new file mode 100644 index 0000000..79cde8e --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +GITHUB_TOKEN +COVERALLS_REPO_TOKEN diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d16d9d4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,104 @@ +name: test + +on: + push: + pull_request: + types: [review_requested, ready_for_review] + +jobs: + # ************************************* + # ************* Pre-commit ************ + # ************************************* + pre-commit: + name: pre-commit ${{ matrix.python-version }} - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + max-parallel: 5 + matrix: + os: + - ubuntu-22.04 + python-version: + - "3.11" +# - "3.10" +# - "3.9" +# - "3.8" + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install detect-secrets + run: pip install --no-cache-dir detect-secrets doc8 isort==5.11.5 + - name: Run pre-commit + uses: pre-commit/action@v3.0.0 + + # ************************************* + # **************** Tests ************** + # ************************************* + test: + needs: pre-commit + name: test ${{ matrix.python-version }} - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + max-parallel: 5 + matrix: + os: + - ubuntu-22.04 + # - Windows + # - MacOs + python-version: +# - "3.12" + - "3.11" + - "3.10" + - "3.9" + steps: + - name: Clean-up + run: sudo apt clean && sudo apt autoclean && sudo rm -rf /tmp/* && sudo rm -rf /usr/share/dotnet && sudo rm -rf /opt/ghc && sudo rm -rf "/usr/local/share/boost" && sudo rm -rf "$AGENT_TOOLSDIRECTORY" + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + - name: Install package + run: | + pip install -e .[all] + - name: Run test suite + run: pytest -vrx + env: + PYTEST_ADDOPTS: "-vv --durations=10" + - name: Coveralls + id: coveralls-setup + continue-on-error: true + uses: AndreMiras/coveralls-python-action@develop + with: + parallel: true + flag-name: Run Tests + + # ************************************* + # ************** Coveralls ************ + # ************************************* + coveralls_finish: + name: coveralls_finish + needs: test + runs-on: ubuntu-latest + steps: + - name: Install dependencies + run: | + python -m pip install pyyaml + - name: Coveralls Finished + id: coveralls-finish + continue-on-error: true +# if: steps.coveralls-setup.outcome == 'success' + env: + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + GITHUB_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + uses: AndreMiras/coveralls-python-action@develop + with: + parallel-finished: true + debug: true diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..7990675 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +*.py[cod] +.hgignore~ +.gitignore~ +.hg/ +.hgtags +.tox/ +.cache/ +.coverage* +!.coveragerc +/htmlcov/ +*.py,cover +.idea/ +.vscode/ +.pytest_cache/ +/coverage.xml +.eggs/ + +MANIFEST.in~ +codebin/ +/tmp/ +.zip +/examples/db/ +/var/ +/examples/tmp/ +/examples/logs/ +/builddocs/ +/docs/_build/ +/builddocs.zip +/build/ +/dist/ +fake_py_django_storage.egg-info +matyan.log* +db.sqlite3 +sample_db.sqlite +test_database.db +local_settings.py +/prof/ +*.cast +.ipynb_checkpoints/ +.scannerwork/ +*db.sqlite3* +examples/pydantic/media/ +examples/tortoise/media/ +examples/django/media/ +examples/dataclasses/media/ +examples/sqlalchemy/media/ +examples/sqlmodel/media/ +examples/hypothesis/.hypothesis/ +.mypy_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5e2a6e5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,68 @@ +exclude: "^docs/|/migrations/" +default_stages: [ commit, push ] +default_language_version: + python: python3 + +repos: + + - repo: local + hooks: + - id: detect-secrets + name: Detect secrets + language: python + entry: detect-secrets-hook + args: ['--baseline', '.secrets.baseline'] + + - id: doc8 + name: Doc8 linter + language: python + entry: doc8 + args: [] + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + exclude: "data/" + + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-added-large-files + - id: debug-statements + - id: check-merge-conflict + + - repo: https://github.com/psf/black + rev: 24.4.2 + hooks: + - id: black + name: black + files: . + args: [ "--config", "pyproject.toml" ] + + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + name: isort + files: . + args: [ "--settings-path", "pyproject.toml", "--profile=black" ] + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.4.8 + hooks: + - id: ruff + name: lint + files: . + args: [ "--config", "pyproject.toml" ] + + - repo: https://github.com/jsh9/pydoclint + rev: 0.4.2 + hooks: + - id: pydoclint + +# - repo: https://github.com/asottile/pyupgrade +# rev: v3.2.0 +# hooks: +# - id: pyupgrade +# args: [ --py310-plus ] diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..2e68790 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,15 @@ +- id: detect-secrets + name: Detect secrets + description: Detects high entropy strings that are likely to be passwords. + entry: detect-secrets-hook + language: python + # for backward compatibility + files: .* + +- id: doc8 + name: doc8 + description: This hook runs doc8 for linting docs + entry: doc8 + language: python + files: \.rst$ + require_serial: true diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..27dab1f --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,35 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.10" + # You can also specify other tool versions: + # nodejs: "20" + # rust: "1.70" + # golang: "1.20" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + +# Optionally build your docs in additional formats such as PDF and ePub +formats: + - pdf + - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..c9298e2 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,166 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": { + "Makefile": [ + { + "type": "Secret Keyword", + "filename": "Makefile", + "hashed_secret": "ee783f2421477b5483c23f47eca1f69a1f2bf4fb", + "is_verified": true, + "line_number": 74 + }, + { + "type": "Secret Keyword", + "filename": "Makefile", + "hashed_secret": "1457a35245051927fac6fa556074300f4162ed66", + "is_verified": true, + "line_number": 77 + } + ], + "examples/django/article/tests.py": [ + { + "type": "Secret Keyword", + "filename": "examples/django/article/tests.py", + "hashed_secret": "9bc34549d565d9505b287de0cd20ac77be1d3f2c", + "is_verified": true, + "line_number": 96 + } + ], + "fakepy/django_storage/tests/data.py": [ + { + "type": "Private Key", + "filename": "fakepy/django_storage/tests/data.py", + "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", + "is_verified": true, + "line_number": 10 + } + ] + }, + "generated_at": "2024-08-13T00:22:03Z" +} diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..8eef41b --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,23 @@ +Release history and notes +========================= + +`Sequence based identifiers +`_ +are used for versioning (schema follows below): + +.. code-block:: text + + major.minor[.revision] + +- It's always safe to upgrade within the same minor version (for example, from + 0.3 to 0.3.4). +- Minor and major version changes might be backwards incompatible. Read the + release notes carefully before upgrading (for example, when upgrading from + 0.3.4 to 0.4). +- All backwards incompatible changes are mentioned in this document. + +0.1 +----- +2024-08-14 + +- Initial beta release. diff --git a/CODE_OF_CONDUCT.rst b/CODE_OF_CONDUCT.rst new file mode 100644 index 0000000..bbbcfb9 --- /dev/null +++ b/CODE_OF_CONDUCT.rst @@ -0,0 +1,144 @@ +Contributor Covenant Code of Conduct +==================================== + +Our Pledge +---------- + +We as members, contributors, and leaders pledge to make participation in +our community a harassment-free experience for everyone, regardless of +age, body size, visible or invisible disability, ethnicity, sex +characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, +welcoming, diverse, inclusive, and healthy community. + +Our Standards +------------- + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our + mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political + attacks +- Public or private harassment +- Publishing others’ private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +Enforcement Responsibilities +---------------------------- + +Community leaders are responsible for clarifying and enforcing our +standards of acceptable behavior and will take appropriate and fair +corrective action in response to any behavior that they deem +inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other +contributions that are not aligned to this Code of Conduct, and will +communicate reasons for moderation decisions when appropriate. + +Scope +----- + +This Code of Conduct applies within all community spaces, and also +applies when an individual is officially representing the community in +public spaces. Examples of representing our community include using an +official e-mail address, posting via an official social media account, +or acting as an appointed representative at an online or offline event. + +Enforcement +----------- + +Instances of abusive, harassing, or otherwise unacceptable behavior may +be reported to the community leaders responsible for enforcement at +artur.barseghyan@gmail.com. All complaints will be reviewed and +investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security +of the reporter of any incident. + +Enforcement Guidelines +---------------------- + +Community leaders will follow these Community Impact Guidelines in +determining the consequences for any action they deem in violation of +this Code of Conduct: + +1. Correction +~~~~~~~~~~~~~ + +**Community Impact**: Use of inappropriate language or other behavior +deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, +providing clarity around the nature of the violation and an explanation +of why the behavior was inappropriate. A public apology may be +requested. + +2. Warning +~~~~~~~~~~ + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, for a specified period of +time. This includes avoiding interactions in community spaces as well as +external channels like social media. Violating these terms may lead to a +temporary or permanent ban. + +3. Temporary Ban +~~~~~~~~~~~~~~~~ + +**Community Impact**: A serious violation of community standards, +including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No +public or private interaction with the people involved, including +unsolicited interaction with those enforcing the Code of Conduct, is +allowed during this period. Violating these terms may lead to a +permanent ban. + +4. Permanent Ban +~~~~~~~~~~~~~~~~ + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of +individuals. + +**Consequence**: A permanent ban from any sort of public interaction +within the community. + +Attribution +----------- + +This Code of Conduct is adapted from the `Contributor +Covenant `__, version 2.0, +available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by `Mozilla’s code of conduct +enforcement ladder `__. + +For answers to common questions about this code of conduct, see the FAQ +at https://www.contributor-covenant.org/faq. Translations are available +at https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..7b201f9 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,126 @@ +Contributor guidelines +====================== + +.. _documentation: https://fake-py-pathy-storage.readthedocs.io/#writing-documentation +.. _testing: https://fake-py-pathy-storage.readthedocs.io/#testing +.. _pre-commit: https://pre-commit.com/#installation +.. _black: https://black.readthedocs.io/ +.. _isort: https://pycqa.github.io/isort/ +.. _doc8: https://doc8.readthedocs.io/ +.. _ruff: https://beta.ruff.rs/docs/ +.. _pip-tools: https://pip-tools.readthedocs.io/ +.. _issues: https://github.com/barseghyanartur/fake-py-pathy-storage/issues +.. _discussions: https://github.com/barseghyanartur/fake-py-pathy-storage/discussions +.. _pull request: https://github.com/barseghyanartur/fake-py-pathy-storage/pulls +.. _support: https://fake-py-pathy-storage.readthedocs.io/#support +.. _installation: https://fake-py-pathy-storage.readthedocs.io/#installation +.. _features: https://fake-py-pathy-storage.readthedocs.io/#features +.. _prerequisites: https://fake-py-pathy-storage.readthedocs.io/#prerequisites + +Developer prerequisites +----------------------- +pre-commit +~~~~~~~~~~ +Refer to `pre-commit`_ for installation instructions. + +TL;DR: + +.. code-block:: sh + + pip install pipx --user # Install pipx + pipx install pre-commit # Install pre-commit + pre-commit install # Install pre-commit hooks + +Installing `pre-commit`_ will ensure you adhere to the project code quality +standards. + +Code standards +-------------- +`black`_, `isort`_, `ruff`_ and `doc8`_ will be automatically triggered by +`pre-commit`_. Still, if you want to run checks manually: + +.. code-block:: sh + + make black + make doc8 + make isort + make ruff + +Requirements +------------ +Requirements are compiled using `pip-tools`_. + +.. code-block:: sh + + make compile-requirements + +Virtual environment +------------------- +You are advised to work in virtual environment. + +TL;DR: + +.. code-block:: sh + + python -m venv env + pip install -e .[all] + +Documentation +------------- +Check `documentation`_. + +Testing +------- +Check `testing`_. + +If you introduce changes or fixes, make sure to test them locally using +all supported environments. For that use tox. + +.. code-block:: sh + + tox + +In any case, GitHub Actions will catch potential errors, but using tox speeds +things up. + +For a quick test of the package and all examples, use the following `Makefile` +command: + +.. code-block:: sh + + make test-all + +Pull requests +------------- +You can contribute to the project by making a `pull request`_. + +For example: + +- To fix documentation typos. +- To improve documentation (for instance, to add new recipe or fix + an existing recipe that doesn't seem to work). +- To introduce a new feature (for instance, add support for a non-supported + file type). + +**General list to go through:** + +- Does your change require documentation update? +- Does your change require update to tests? + +**When fixing bugs (in addition to the general list):** + +- Make sure to add regression tests. + +**When adding a new feature (in addition to the general list):** + +- Make sure to update the documentation. + +Questions +--------- +Questions can be asked on GitHub `discussions`_. + +Issues +------ +For reporting a bug or filing a feature request use GitHub `issues`_. + +**Do not report security issues on GitHub**. Check the `support`_ section. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..74a9269 --- /dev/null +++ b/Makefile @@ -0,0 +1,132 @@ +# Update version ONLY here +VERSION := 0.1 +SHELL := /bin/bash +# Makefile for project +VENV := ~/.virtualenvs/fake-py-django-storage/bin/activate + +# Build documentation using Sphinx and zip it +build_docs: + source $(VENV) && sphinx-build -n -a -b html docs builddocs + cd builddocs && zip -r ../builddocs.zip . -x ".*" && cd .. + +rebuild_docs: + source $(VENV) && sphinx-apidoc fakepy/django_storage --full -o docs -H 'fake-py-django-storage' -A 'Artur Barseghyan ' -f -d 20 + cp docs/conf.py.distrib docs/conf.py + cp docs/index.rst.distrib docs/index.rst + +build_docs_epub: + $(MAKE) -C docs/ epub + +build_docs_pdf: + $(MAKE) -C docs/ latexpdf + +pre-commit: + pre-commit run --all-files + +# Format code using Black +black: + source $(VENV) && black . + +# Sort imports using isort +isort: + source $(VENV) && isort . --overwrite-in-place + +doc8: + source $(VENV) && doc8 + +# Run ruff on the codebase +ruff: + source $(VENV) && ruff . + +# Serve the built docs on port 5001 +serve_docs: + source $(VENV) && cd builddocs && python -m http.server 5001 + +# Install the project +install: + source $(VENV) && pip install -e .[all] + +test: clean + source $(VENV) && pytest -vrx -s + +test-all: test \ + django-test + +django-test: + source $(VENV) && cd examples/django/ && ./manage.py test + +shell: + source $(VENV) && ipython + +django-shell: + source $(VENV) && python examples/django/manage.py shell + +django-runserver: + source $(VENV) && python examples/django/manage.py runserver 0.0.0.0:8000 --traceback -v 3 + +django-makemigrations: + source $(VENV) && python examples/django/manage.py makemigrations + +django-apply-migrations: + source $(VENV) && python examples/django/manage.py migrate + +create-secrets: + source $(VENV) && detect-secrets scan > .secrets.baseline + +detect-secrets: + source $(VENV) && detect-secrets scan --baseline .secrets.baseline + +# Clean up generated files +clean: + find . -type f -name "*.pyc" -exec rm -f {} \; + find . -type f -name "builddocs.zip" -exec rm -f {} \; + find . -type f -name "*.py,cover" -exec rm -f {} \; + find . -type f -name "*.orig" -exec rm -f {} \; + find . -type f -name "*.db" -exec rm -f {} \; + find . -type d -name "__pycache__" -exec rm -rf {} \; -prune + rm -rf build/ + rm -rf dist/ + rm -rf .cache/ + rm -rf htmlcov/ + rm -rf builddocs/ + rm -rf testdocs/ + rm -rf .coverage + rm -rf .pytest_cache/ + rm -rf .mypy_cache/ + rm -rf .ruff_cache/ + rm -rf dist/ + rm -rf fake-py-django-storage.egg-info/ + +compile-requirements-pip-tools: + source $(VENV) && python -m piptools compile --all-extras -o docs/requirements.txt pyproject.toml + +compile-requirements-upgrade-pip-tools: + source $(VENV) && python -m piptools compile --all-extras -o docs/requirements.txt pyproject.toml --upgrade + +compile-requirements: + source $(VENV) && uv pip compile --all-extras -o docs/requirements.txt pyproject.toml + +compile-requirements-upgrade: + source $(VENV) && uv pip compile --all-extras -o docs/requirements.txt pyproject.toml --upgrade + +update-version: + sed -i 's/version = "[0-9.]\+"/version = "$(VERSION)"/' pyproject.toml + sed -i 's/__version__ = "[0-9.]\+"/__version__ = "$(VERSION)"/' fakepy/pathy_storage/__init__.py + +build: + source $(VENV) && python -m build . + +check-build: + source $(VENV) && twine check dist/* + +release: + source $(VENV) && twine upload dist/* --verbose + +test-release: + source $(VENV) && twine upload --repository testpypi dist/* + +mypy: + source $(VENV) && mypy fakepy/pathy_storage/*.py + +%: + @: diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..f9f42ba --- /dev/null +++ b/README.rst @@ -0,0 +1,187 @@ +====================== +fake-py-django-storage +====================== +.. External references + +.. _fake.py: https://fakepy.readthedocs.io +.. _faker-file: https://faker-file.readthedocs.io +.. _Django: https://www.djangoproject.com +.. _django-storages: https://django-storages.readthedocs.io + +.. Internal references + +.. _fake-py-django-storage: https://github.com/barseghyanartur/fake-py-django-storage +.. _Read the Docs: http://fake-py-django-storage.readthedocs.io +.. _Contributor guidelines: https://fakepy.readthedocs.io/en/latest/contributor_guidelines.html + +`Django`_ storage for `fake.py`_. + +.. image:: https://img.shields.io/pypi/v/fake-py-django-storage.svg + :target: https://pypi.python.org/pypi/fake-py-django-storage + :alt: PyPI Version + +.. image:: https://img.shields.io/pypi/pyversions/fake-py-django-storage.svg + :target: https://pypi.python.org/pypi/fake-py-django-storage/ + :alt: Supported Python versions + +.. image:: https://github.com/barseghyanartur/fake-py-django-storage/actions/workflows/test.yml/badge.svg?branch=main + :target: https://github.com/barseghyanartur/fake-py-django-storage/actions + :alt: Build Status + +.. image:: https://readthedocs.org/projects/fake-py-django-storage/badge/?version=latest + :target: http://fake-py-django-storage.readthedocs.io + :alt: Documentation Status + +.. image:: https://img.shields.io/badge/license-MIT-blue.svg + :target: https://github.com/barseghyanartur/fake-py-django-storage/#License + :alt: MIT + +.. image:: https://coveralls.io/repos/github/barseghyanartur/fake-py-django-storage/badge.svg?branch=main&service=github + :target: https://coveralls.io/github/barseghyanartur/fake-py-django-storage?branch=main + :alt: Coverage + +`fake-py-django-storage`_ is a `Django`_ storage integration for `fake.py`_ - a +standalone, portable library designed for generating various +random data types for testing. + +Features +======== +- Almost seamless integration with `Django`_ (and `django-storages`_). + +Prerequisites +============= +Python 3.9+ + +Installation +============ + +.. code-block:: sh + + pip install fake-py-django-storage + +Documentation +============= +- Documentation is available on `Read the Docs`_. +- For guidelines on contributing check the `Contributor guidelines`_. + +Usage +===== +`FileSystemStorage` of `Django` +------------------------------- +.. code-block:: python + + from fake import FAKER + from fakepy.django_storage.filesystem import DjangoFileSystemStorage + + STORAGE = DjangoFileSystemStorage( + root_path="tmp", # Optional + rel_path="sub-tmp", # Optional + ) + + pdf_file = FAKER.pdf_file(storage=STORAGE) + + STORAGE.exists(pdf_file) + +AWS S3 (using `django-storages`) +-------------------------------- +.. code-block:: python + + from fake import FAKER + from fakepy.django_storage.aws_s3 import DjangoAWSS3Storage + + STORAGE = DjangoAWSS3Storage( + root_path="tmp", # Optional + rel_path="sub-tmp", # Optional + ) + + pdf_file = FAKER.pdf_file(storage=STORAGE) + + STORAGE.exists(pdf_file) + +Google Cloud Storage (using `django-storages`) +---------------------------------------------- +.. code-block:: python + + from fake import FAKER + from fakepy.django_storage.google_cloud_storage import ( + DjangoGoogleCloudStorage, + ) + + STORAGE = DjangoGoogleCloudStorage( + root_path="tmp", # Optional + rel_path="sub-tmp", # Optional + ) + + pdf_file = FAKER.pdf_file(storage=STORAGE) + + STORAGE.exists(pdf_file) + +Azure Cloud Storage (using `django-storages`) +--------------------------------------------- +.. code-block:: python + + from fake import FAKER + from fakepy.django_storage.azure_cloud_storage import ( + DjangoAzureCloudStorage, + ) + + STORAGE = DjangoAzureCloudStorage( + root_path="tmp", # Optional + rel_path="sub-tmp", # Optional + ) + + pdf_file = FAKER.pdf_file(storage=STORAGE) + + STORAGE.exists(pdf_file) + +Tests +===== + +.. code-block:: sh + + pytest + +Writing documentation +===================== + +Keep the following hierarchy. + +.. code-block:: text + + ===== + title + ===== + + header + ====== + + sub-header + ---------- + + sub-sub-header + ~~~~~~~~~~~~~~ + + sub-sub-sub-header + ^^^^^^^^^^^^^^^^^^ + + sub-sub-sub-sub-header + ++++++++++++++++++++++ + + sub-sub-sub-sub-sub-header + ************************** + +License +======= + +MIT + +Support +======= +For security issues contact me at the e-mail given in the `Author`_ section. + +For overall issues, go to `GitHub `_. + +Author +====== + +Artur Barseghyan diff --git a/SECURITY.rst b/SECURITY.rst new file mode 100644 index 0000000..0e5cf01 --- /dev/null +++ b/SECURITY.rst @@ -0,0 +1,35 @@ +Security Policy +=============== +Reporting a Vulnerability +------------------------- +**Do not report security issues on GitHub!** + +Please report security issues by emailing Artur Barseghyan +. + +Supported Versions +------------------ +The two most recent ``fake-py-pathy-storage`` release series receive security +support. +It's recommended to use the latest version. + +.. code-block:: text + + ┌─────────────────┬────────────────┐ + │ Version │ Supported │ + ├─────────────────┼────────────────┤ + │ 0.2.x │ Yes │ + ├─────────────────┼────────────────┤ + │ 0.1.x │ Yes │ + ├─────────────────┼────────────────┤ + │ < 0.1 │ No │ + └─────────────────┴────────────────┘ + +.. note:: + + For example, during the development cycle leading to the release + of ``fake-py-pathy-storage`` 0.17.x, support will be provided + for ``fake-py-pathy-storage`` 0.16.x. + + Upon the release of ``fake-py-pathy-storage`` 0.18.x, security support + for ``fake-py-pathy-storage`` 0.16.x will end. diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..ac59673 --- /dev/null +++ b/conftest.py @@ -0,0 +1,52 @@ +""" +Configuration hooks for `pytest`. Normally this wouldn't be necessary, +but since `pytest-rst` is used, we want to clean-up files generated by +running documentation tests. Therefore, this hook, which simply +calls the `clean_up` method of the `FILE_REGISTRY` instance. +""" + +import pytest +from fake import FILE_REGISTRY + +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ( + "pytest_runtest_setup", + "pytest_runtest_teardown", +) + + +def pytest_collection_modifyitems(session, config, items): + """Modify test items during collection.""" + for item in items: + try: + from pytest_rst import RSTTestItem + + if isinstance(item, RSTTestItem): + # Dynamically add marker to RSTTestItem tests + item.add_marker(pytest.mark.django_db(transaction=True)) + except ImportError: + pass + + +def pytest_runtest_setup(item): + """Setup before test runs.""" + try: + from pytest_rst import RSTTestItem + + if isinstance(item, RSTTestItem): + pass + except ImportError: + pass + + +def pytest_runtest_teardown(item, nextitem): + """Clean up after test ends.""" + try: + from pytest_rst import RSTTestItem + + if isinstance(item, RSTTestItem): + FILE_REGISTRY.clean_up() + except ImportError: + pass diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..565b052 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGELOG.rst diff --git a/docs/code_of_conduct.rst b/docs/code_of_conduct.rst new file mode 100644 index 0000000..96e0ba2 --- /dev/null +++ b/docs/code_of_conduct.rst @@ -0,0 +1 @@ +.. include:: ../CODE_OF_CONDUCT.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100755 index 0000000..57b9b53 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,89 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html +import os +import sys +from pathlib import Path + +PARENT_DIR = Path(os.path.abspath("..")) +sys.path.insert(0, PARENT_DIR.name) +sys.path.insert(0, (PARENT_DIR / "fakepy" / "django_storage").name) + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +try: + from fakepy import django_storage + + version = django_storage.__version__ + project = django_storage.__title__ + copyright = django_storage.__copyright__ + author = django_storage.__author__ +except ImportError: + version = "0.1" + project = "fake-py-django-storage" + copyright = "2024, Artur Barseghyan " + author = "Artur Barseghyan " + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.todo", + "sphinx_no_pragma", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +language = "en" + +release = version + +# The suffix of source filenames. +source_suffix = { + ".rst": "restructuredtext", +} + +pygments_style = "sphinx" + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] +# html_extra_path = ["examples"] + +prismjs_base = "//cdnjs.cloudflare.com/ajax/libs/prism/1.29.0" + +html_css_files = [ + f"{prismjs_base}/themes/prism.min.css", + f"{prismjs_base}/plugins/toolbar/prism-toolbar.min.css", + "https://cdn.jsdelivr.net/gh/barseghyanartur/jsphinx@1.3.4/src/css/sphinx_rtd_theme.css", # noqa +] + +html_js_files = [ + f"{prismjs_base}/prism.min.js", + f"{prismjs_base}/plugins/autoloader/prism-autoloader.min.js", + f"{prismjs_base}/plugins/toolbar/prism-toolbar.min.js", + f"{prismjs_base}/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js", + "https://cdn.jsdelivr.net/gh/barseghyanartur/jsphinx@1.3.4/src/js/download_adapter.js", # noqa +] + +# -- Options for todo extension ---------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/todo.html#configuration + +todo_include_todos = True + +# -- Options for Epub output ---------------------------------------------- +epub_title = project +epub_author = author +epub_publisher = "GitHub" +epub_copyright = copyright +# URL or ISBN +epub_identifier = "https://github.com/barseghyanartur/fake-py-django-storage" +epub_scheme = "URL" # or "ISBN" +epub_uid = "https://github.com/barseghyanartur/fake-py-django-storage" diff --git a/docs/conf.py.distrib b/docs/conf.py.distrib new file mode 100755 index 0000000..57b9b53 --- /dev/null +++ b/docs/conf.py.distrib @@ -0,0 +1,89 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html +import os +import sys +from pathlib import Path + +PARENT_DIR = Path(os.path.abspath("..")) +sys.path.insert(0, PARENT_DIR.name) +sys.path.insert(0, (PARENT_DIR / "fakepy" / "django_storage").name) + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +try: + from fakepy import django_storage + + version = django_storage.__version__ + project = django_storage.__title__ + copyright = django_storage.__copyright__ + author = django_storage.__author__ +except ImportError: + version = "0.1" + project = "fake-py-django-storage" + copyright = "2024, Artur Barseghyan " + author = "Artur Barseghyan " + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.todo", + "sphinx_no_pragma", +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +language = "en" + +release = version + +# The suffix of source filenames. +source_suffix = { + ".rst": "restructuredtext", +} + +pygments_style = "sphinx" + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] +# html_extra_path = ["examples"] + +prismjs_base = "//cdnjs.cloudflare.com/ajax/libs/prism/1.29.0" + +html_css_files = [ + f"{prismjs_base}/themes/prism.min.css", + f"{prismjs_base}/plugins/toolbar/prism-toolbar.min.css", + "https://cdn.jsdelivr.net/gh/barseghyanartur/jsphinx@1.3.4/src/css/sphinx_rtd_theme.css", # noqa +] + +html_js_files = [ + f"{prismjs_base}/prism.min.js", + f"{prismjs_base}/plugins/autoloader/prism-autoloader.min.js", + f"{prismjs_base}/plugins/toolbar/prism-toolbar.min.js", + f"{prismjs_base}/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js", + "https://cdn.jsdelivr.net/gh/barseghyanartur/jsphinx@1.3.4/src/js/download_adapter.js", # noqa +] + +# -- Options for todo extension ---------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/todo.html#configuration + +todo_include_todos = True + +# -- Options for Epub output ---------------------------------------------- +epub_title = project +epub_author = author +epub_publisher = "GitHub" +epub_copyright = copyright +# URL or ISBN +epub_identifier = "https://github.com/barseghyanartur/fake-py-django-storage" +epub_scheme = "URL" # or "ISBN" +epub_uid = "https://github.com/barseghyanartur/fake-py-django-storage" diff --git a/docs/contributor_guidelines.rst b/docs/contributor_guidelines.rst new file mode 100644 index 0000000..e582053 --- /dev/null +++ b/docs/contributor_guidelines.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/docs/django_storage.rst b/docs/django_storage.rst new file mode 100644 index 0000000..ce4009f --- /dev/null +++ b/docs/django_storage.rst @@ -0,0 +1,69 @@ +django\_storage package +======================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 20 + + django_storage.tests + +Submodules +---------- + +pathy\_storage.aws\_s3 module +----------------------------- + +.. automodule:: django_storage.aws_s3 + :members: + :undoc-members: + :show-inheritance: + +pathy\_storage.azure\_cloud\_storage module +------------------------------------------- + +.. automodule:: django_storage.azure_cloud_storage + :members: + :undoc-members: + :show-inheritance: + +pathy\_storage.base module +-------------------------- + +.. automodule:: django_storage.base + :members: + :undoc-members: + :show-inheritance: + +pathy\_storage.cloud module +--------------------------- + +.. automodule:: django_storage.cloud + :members: + :undoc-members: + :show-inheritance: + +pathy\_storage.filesystem module +-------------------------------- + +.. automodule:: django_storage.filesystem + :members: + :undoc-members: + :show-inheritance: + +pathy\_storage.google\_cloud\_storage module +-------------------------------------------- + +.. automodule:: django_storage.google_cloud_storage + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: django_storage + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/django_storage.tests.rst b/docs/django_storage.tests.rst new file mode 100644 index 0000000..0e261bb --- /dev/null +++ b/docs/django_storage.tests.rst @@ -0,0 +1,37 @@ +django\_storage.tests package +============================= + +Submodules +---------- + +django\_storage.tests.data module +--------------------------------- + +.. automodule:: django_storage.tests.data + :members: + :undoc-members: + :show-inheritance: + +django\_storage.tests.test\_aws\_s3\_storage module +--------------------------------------------------- + +.. automodule:: django_storage.tests.test_aws_s3_storage + :members: + :undoc-members: + :show-inheritance: + +django\_storage.tests.test\_filesystem\_storage module +------------------------------------------------------ + +.. automodule:: django_storage.tests.test_filesystem_storage + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: django_storage.tests + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/documentation.rst b/docs/documentation.rst new file mode 100755 index 0000000..3262f92 --- /dev/null +++ b/docs/documentation.rst @@ -0,0 +1,16 @@ + +Project documentation +===================== +Contents: + +.. contents:: Table of Contents + +.. toctree:: + :maxdepth: 2 + + index + security + contributor_guidelines + code_of_conduct + changelog + package diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..d00c3c7 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,2 @@ +.. include:: ../README.rst +.. include:: documentation.rst diff --git a/docs/index.rst.distrib b/docs/index.rst.distrib new file mode 100644 index 0000000..d00c3c7 --- /dev/null +++ b/docs/index.rst.distrib @@ -0,0 +1,2 @@ +.. include:: ../README.rst +.. include:: documentation.rst diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/package.rst b/docs/package.rst new file mode 100644 index 0000000..3ca9239 --- /dev/null +++ b/docs/package.rst @@ -0,0 +1,15 @@ + +Package +======= + +.. toctree:: + :maxdepth: 20 + + django_storage + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..b396699 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,337 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --all-extras -o docs/requirements.txt pyproject.toml +alabaster==0.7.16 + # via sphinx +asgiref==3.8.1 + # via django +asttokens==2.4.1 + # via stack-data +azure-core==1.30.2 + # via azure-storage-blob +azure-storage-blob==12.22.0 + # via pathy +babel==2.16.0 + # via sphinx +backports-tarfile==1.2.0 + # via jaraco-context +black==24.8.0 + # via fake-py-pathy-storage (pyproject.toml) +boto3==1.34.158 + # via pathy +botocore==1.34.158 + # via + # boto3 + # s3transfer +build==1.2.1 + # via pip-tools +cachetools==5.4.0 + # via google-auth +certifi==2024.7.4 + # via requests +cffi==1.17.0 + # via cryptography +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via + # black + # pip-tools + # pydoclint + # typer +coverage==7.6.1 + # via pytest-cov +cryptography==43.0.0 + # via + # azure-storage-blob + # secretstorage +decorator==5.1.1 + # via ipython +detect-secrets==1.5.0 + # via fake-py-pathy-storage (pyproject.toml) +django==5.1 + # via fake-py-pathy-storage (pyproject.toml) +doc8==1.1.1 + # via fake-py-pathy-storage (pyproject.toml) +docstring-parser-fork==0.0.9 + # via pydoclint +docutils==0.19 + # via + # doc8 + # readme-renderer + # restructuredtext-lint + # sphinx + # sphinx-no-pragma + # sphinx-rtd-theme +exceptiongroup==1.2.2 + # via + # ipython + # pytest +executing==2.0.1 + # via stack-data +google-api-core==2.19.1 + # via + # google-cloud-core + # google-cloud-storage +google-auth==2.33.0 + # via + # google-api-core + # google-cloud-core + # google-cloud-storage +google-cloud-core==2.4.1 + # via google-cloud-storage +google-cloud-storage==1.44.0 + # via pathy +google-crc32c==1.5.0 + # via google-resumable-media +google-resumable-media==2.7.2 + # via google-cloud-storage +googleapis-common-protos==1.63.2 + # via google-api-core +idna==3.7 + # via requests +imagesize==1.4.1 + # via sphinx +importlib-metadata==8.2.0 + # via + # keyring + # twine +iniconfig==2.0.0 + # via pytest +ipython==8.26.0 + # via fake-py-pathy-storage (pyproject.toml) +isodate==0.6.1 + # via azure-storage-blob +isort==5.13.2 + # via fake-py-pathy-storage (pyproject.toml) +jaraco-classes==3.4.0 + # via keyring +jaraco-context==5.3.0 + # via keyring +jaraco-functools==4.0.2 + # via keyring +jedi==0.19.1 + # via ipython +jeepney==0.8.0 + # via + # keyring + # secretstorage +jinja2==3.1.4 + # via sphinx +jmespath==1.0.1 + # via + # boto3 + # botocore +keyring==25.3.0 + # via twine +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.5 + # via jinja2 +matplotlib-inline==0.1.7 + # via ipython +mdurl==0.1.2 + # via markdown-it-py +more-itertools==10.4.0 + # via + # jaraco-classes + # jaraco-functools +mypy==1.11.1 + # via fake-py-pathy-storage (pyproject.toml) +mypy-extensions==1.0.0 + # via + # black + # mypy +nh3==0.2.18 + # via readme-renderer +packaging==24.1 + # via + # black + # build + # pytest + # sphinx +parametrize==0.1.1 + # via fake-py-pathy-storage (pyproject.toml) +parso==0.8.4 + # via jedi +pathspec==0.12.1 + # via black +pathy==0.10.3 + # via fake-py-pathy-storage (pyproject.toml) +pbr==6.0.0 + # via stevedore +pexpect==4.9.0 + # via ipython +pip==24.2 + # via pip-tools +pip-tools==7.4.1 + # via fake-py-pathy-storage (pyproject.toml) +pkginfo==1.10.0 + # via twine +platformdirs==4.2.2 + # via black +pluggy==1.5.0 + # via pytest +prompt-toolkit==3.0.47 + # via ipython +proto-plus==1.24.0 + # via google-api-core +protobuf==5.27.3 + # via + # google-api-core + # google-cloud-storage + # googleapis-common-protos + # proto-plus +ptyprocess==0.7.0 + # via pexpect +pure-eval==0.2.3 + # via stack-data +pyasn1==0.6.0 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.0 + # via google-auth +pycparser==2.22 + # via cffi +pydoclint==0.5.6 + # via fake-py-pathy-storage (pyproject.toml) +pygments==2.18.0 + # via + # doc8 + # ipython + # readme-renderer + # rich + # sphinx +pyproject-hooks==1.1.0 + # via + # build + # pip-tools +pytest==8.3.2 + # via + # fake-py-pathy-storage (pyproject.toml) + # pytest-cov + # pytest-django +pytest-cov==5.0.0 + # via fake-py-pathy-storage (pyproject.toml) +pytest-django==4.8.0 + # via fake-py-pathy-storage (pyproject.toml) +pytest-rst==0.1.5 + # via fake-py-pathy-storage (pyproject.toml) +python-dateutil==2.9.0.post0 + # via botocore +pyyaml==6.0.2 + # via detect-secrets +readme-renderer==43.0 + # via twine +requests==2.32.3 + # via + # azure-core + # detect-secrets + # google-api-core + # google-cloud-storage + # requests-toolbelt + # sphinx + # twine +requests-toolbelt==1.0.0 + # via twine +restructuredtext-lint==1.4.0 + # via doc8 +rfc3986==2.0.0 + # via twine +rich==13.7.1 + # via + # twine + # typer +rsa==4.9 + # via google-auth +ruff==0.5.7 + # via fake-py-pathy-storage (pyproject.toml) +s3transfer==0.10.2 + # via boto3 +secretstorage==3.3.3 + # via keyring +setuptools==72.1.0 + # via pip-tools +shellingham==1.5.4 + # via typer +six==1.16.0 + # via + # asttokens + # azure-core + # google-cloud-storage + # isodate + # python-dateutil +smart-open==6.4.0 + # via pathy +snowballstemmer==2.2.0 + # via sphinx +sphinx==5.3.0 + # via + # fake-py-pathy-storage (pyproject.toml) + # sphinx-no-pragma + # sphinx-rtd-theme + # sphinxcontrib-jquery +sphinx-no-pragma==0.1.1 + # via fake-py-pathy-storage (pyproject.toml) +sphinx-rtd-theme==2.0.0 + # via fake-py-pathy-storage (pyproject.toml) +sphinxcontrib-applehelp==2.0.0 + # via sphinx +sphinxcontrib-devhelp==2.0.0 + # via sphinx +sphinxcontrib-htmlhelp==2.1.0 + # via sphinx +sphinxcontrib-jquery==4.1 + # via sphinx-rtd-theme +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==2.0.0 + # via sphinx +sphinxcontrib-serializinghtml==2.0.0 + # via sphinx +sqlparse==0.5.1 + # via django +stack-data==0.6.3 + # via ipython +stevedore==5.2.0 + # via doc8 +tomli==2.0.1 + # via + # black + # build + # coverage + # doc8 + # mypy + # pip-tools + # pydoclint + # pytest +traitlets==5.14.3 + # via + # ipython + # matplotlib-inline +twine==5.1.1 + # via fake-py-pathy-storage (pyproject.toml) +typer==0.12.3 + # via pathy +typing-extensions==4.12.2 + # via + # asgiref + # azure-core + # azure-storage-blob + # black + # ipython + # mypy + # typer +urllib3==2.2.2 + # via + # botocore + # requests + # twine +uv==0.2.34 + # via fake-py-pathy-storage (pyproject.toml) +wcwidth==0.2.13 + # via prompt-toolkit +wheel==0.44.0 + # via pip-tools +zipp==3.19.2 + # via importlib-metadata diff --git a/docs/security.rst b/docs/security.rst new file mode 100644 index 0000000..2f60433 --- /dev/null +++ b/docs/security.rst @@ -0,0 +1 @@ +.. include:: ../SECURITY.rst diff --git a/examples/django/README.rst b/examples/django/README.rst new file mode 100644 index 0000000..46e065b --- /dev/null +++ b/examples/django/README.rst @@ -0,0 +1,3 @@ +Django +====== +Example factories for `Django `_ models. diff --git a/examples/django/article/__init__.py b/examples/django/article/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/django/article/admin.py b/examples/django/article/admin.py new file mode 100644 index 0000000..dd24c7c --- /dev/null +++ b/examples/django/article/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin + +from article.models import Article + + +@admin.register(Article) +class ArticleAdmin(admin.ModelAdmin): + list_display = ( + "title", + "slug", + "minutes_to_read", + "safe_for_work", + "pub_date", + ) + list_filter = ("safe_for_work",) diff --git a/examples/django/article/apps.py b/examples/django/article/apps.py new file mode 100644 index 0000000..8c0e2c9 --- /dev/null +++ b/examples/django/article/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ArticleConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "article" diff --git a/examples/django/article/factories.py b/examples/django/article/factories.py new file mode 100644 index 0000000..8e8e91a --- /dev/null +++ b/examples/django/article/factories.py @@ -0,0 +1,232 @@ +from typing import Any, Dict + +from django.contrib.auth.models import Group, User +from django.core.files.storage import default_storage +from django.utils import timezone +from django.utils.text import slugify +from fake import ( + FACTORY, + FAKER, + DjangoModelFactory, + PostSave, + PreInit, + PreSave, + SubFactory, + post_save, + pre_init, + pre_save, + trait, +) +from fakepy.django_storage.aws_s3 import DjangoAWSS3Storage +from fakepy.django_storage.azure_cloud_storage import DjangoAzureCloudStorage +from fakepy.django_storage.filesystem import DjangoFileSystemStorage +from fakepy.django_storage.google_cloud_storage import DjangoGoogleCloudStorage +from storages.backends.azure_storage import AzureStorage +from storages.backends.gcloud import GoogleCloudStorage +from storages.backends.s3boto3 import S3Boto3Storage + +from article.models import Article + +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ( + "ArticleFactory", + "GroupFactory", + "UserFactory", +) + +# If we want to support remote storages, we need to manually check which +# file storage backend is used. If `Boto3` storage backend (of +# the `django-storages` package) is used we use the correspondent +# `DjangoS3Storage` class of the `fake-py-django-storage`. +# Otherwise, fall back to native file system storage +# `DjangoFileSystemStorage` of the `fake-py-django-storage`. +if isinstance(default_storage, S3Boto3Storage): + STORAGE = DjangoAWSS3Storage( + rel_path="tmp", + check_bucket=False, + ) +elif isinstance(default_storage, GoogleCloudStorage): + STORAGE = DjangoGoogleCloudStorage( + rel_path="tmp", + check_bucket=False, + ) +elif isinstance(default_storage, AzureStorage): + STORAGE = DjangoAzureCloudStorage( + rel_path="tmp", + check_bucket=False, + ) +else: + STORAGE = DjangoFileSystemStorage( + root_path="testing", + rel_path="tmp", + ) + +CATEGORIES = ( + "art", + "technology", + "literature", +) +TAGS = ( + "painting", + "photography", + "ai", + "data-engineering", + "fiction", + "poetry", + "manual", +) + + +class GroupFactory(DjangoModelFactory): + """Group factory. + + Usage example: + + .. code-block:: python + + group = GroupFactory() + """ + + name = FACTORY.word() + + class Meta: + model = Group + get_or_create = ("name",) + + +def set_password(user: User, password: str) -> None: + user.set_password(password) + + +def add_to_group(user: User, name: str) -> None: + group = GroupFactory(name=name) + user.groups.add(group) + + +def set_username(data: Dict[str, Any]) -> None: + first_name = slugify(data["first_name"]) + last_name = slugify(data["last_name"]) + data["username"] = f"{first_name}_{last_name}_{FAKER.pystr().lower()}" + + +class UserFactory(DjangoModelFactory): + """User factory. + + Usage example: + + .. code-block:: python + + # Create a user. Created user will automatically have his password + # set to "test1234" and will be added to the group "Test group". + user = UserFactory() + + # Create 5 users. + users = UserFactory.create_batch(5) + + # Create a user with custom password + user = UserFactory( + password=PreSave(set_password, password="another-pass"), + ) + + # Add a user to another group + user = UserFactory( + group=PostSave(add_to_group, name="Another group"), + ) + + # Or even add user to multiple groups at once + user = UserFactory( + group_1=PostSave(add_to_group, name="Another group"), + group_2=PostSave(add_to_group, name="Yet another group"), + ) + """ + + username = PreInit(set_username) + first_name = FACTORY.first_name() + last_name = FACTORY.last_name() + email = FACTORY.email() + date_joined = FACTORY.date_time(tzinfo=timezone.get_current_timezone()) + last_login = FACTORY.date_time(tzinfo=timezone.get_current_timezone()) + is_superuser = False + is_staff = False + is_active = FACTORY.pybool() + password = PreSave(set_password, password="test1234") + group = PostSave(add_to_group, name="TestGroup1234") + + class Meta: + model = User + get_or_create = ("username",) + + @post_save + def send_registration_email(self, instance): + """Send an email with registration information.""" + # Your code here + + @trait + def is_admin_user(self, instance: User) -> None: + instance.is_superuser = True + instance.is_staff = True + instance.is_active = True + + @pre_save + def _pre_save_method(self, instance): + # For testing purposes only + instance._pre_save_called = True + + @post_save + def _post_save_method(self, instance): + # For testing purposes only + instance._post_save_called = True + + +def set_headline(data: Dict[str, Any]) -> None: + data["headline"] = data["content"][:25] + + +class ArticleFactory(DjangoModelFactory): + """Article factory. + + Usage example: + + .. code-block:: python + + # Create one article + article = ArticleFactory() + + # Create 5 articles + articles = ArticleFactory.create_batch(5) + + # Create one article with authors username set to admin. + article = ArticleFactory(author__username="admin") + """ + + title = FACTORY.sentence() + slug = FACTORY.slug() + content = FACTORY.text() + headline = PreInit(set_headline) + category = FACTORY.random_choice(elements=CATEGORIES) + pages = FACTORY.pyint(min_value=1, max_value=100) # type: ignore + image = FACTORY.png_file(storage=STORAGE) + pub_date = FACTORY.date(tzinfo=timezone.get_current_timezone()) + safe_for_work = FACTORY.pybool() + minutes_to_read = FACTORY.pyint(min_value=1, max_value=10) + author = SubFactory(UserFactory) + tags = FACTORY.random_sample(elements=TAGS, length=3) + + class Meta: + model = Article + + @pre_init + def set_auto_minutes_to_read(self, data: Dict[str, Any]) -> None: + data["auto_minutes_to_read"] = data["pages"] + + @pre_save + def _pre_save_method(self, instance): + # For testing purposes only + instance._pre_save_called = True + + @post_save + def _post_save_method(self, instance): + # For testing purposes only + instance._post_save_called = True diff --git a/examples/django/article/migrations/0001_initial.py b/examples/django/article/migrations/0001_initial.py new file mode 100644 index 0000000..ec2d4a3 --- /dev/null +++ b/examples/django/article/migrations/0001_initial.py @@ -0,0 +1,57 @@ +# Generated by Django 5.0.6 on 2024-06-07 23:44 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Article", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255)), + ("slug", models.SlugField(unique=True)), + ("content", models.TextField()), + ("headline", models.TextField()), + ("category", models.CharField(max_length=255)), + ("pages", models.IntegerField()), + ("auto_minutes_to_read", models.IntegerField()), + ( + "image", + models.ImageField(blank=True, null=True, upload_to=""), + ), + ( + "pub_date", + models.DateField(default=django.utils.timezone.now), + ), + ("safe_for_work", models.BooleanField(default=False)), + ("minutes_to_read", models.IntegerField(default=5)), + ("tags", models.JSONField(default=list)), + ( + "author", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/examples/django/article/migrations/__init__.py b/examples/django/article/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/django/article/models.py b/examples/django/article/models.py new file mode 100644 index 0000000..2b9e094 --- /dev/null +++ b/examples/django/article/models.py @@ -0,0 +1,29 @@ +from django.conf import settings +from django.db import models +from django.utils import timezone + +__author__ = "Artur Barseghyan " +__copyright__ = "2023-2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("Article",) + + +class Article(models.Model): + title = models.CharField(max_length=255) + slug = models.SlugField(unique=True) + content = models.TextField() + headline = models.TextField() + category = models.CharField(max_length=255) + pages = models.IntegerField() + auto_minutes_to_read = models.IntegerField() + image = models.ImageField(null=True, blank=True) + pub_date = models.DateField(default=timezone.now) + safe_for_work = models.BooleanField(default=False) + minutes_to_read = models.IntegerField(default=5) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE + ) + tags = models.JSONField(default=list) + + def __str__(self): + return self.title diff --git a/examples/django/article/tests.py b/examples/django/article/tests.py new file mode 100644 index 0000000..8acdc28 --- /dev/null +++ b/examples/django/article/tests.py @@ -0,0 +1,111 @@ +from datetime import datetime + +from django.contrib.auth.models import User +from django.test import TestCase +from fake import FILE_REGISTRY + +from article.factories import ArticleFactory, GroupFactory, UserFactory +from article.models import Article + +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("FactoriesTestCase",) + + +class FactoriesTestCase(TestCase): + def tearDown(self): + super().tearDown() + FILE_REGISTRY.clean_up() + + def test_sub_factory(self) -> None: + article = ArticleFactory() + + # Testing SubFactory + self.assertIsInstance(article.author, User) + self.assertIsInstance(article.author.id, int) + self.assertIsInstance(article.author.is_staff, bool) + self.assertIsInstance(article.author.date_joined, datetime) + + # Testing Factory + self.assertIsInstance(article.id, int) + self.assertIsInstance(article.slug, str) + + # Testing hooks + self.assertTrue( + hasattr(article, "_pre_save_called") and article._pre_save_called + ) + self.assertTrue( + hasattr(article, "_post_save_called") and article._post_save_called + ) + self.assertTrue( + hasattr(article.author, "_pre_save_called") + and article.author._pre_save_called + ) + self.assertTrue( + hasattr(article.author, "_post_save_called") + and article.author._post_save_called + ) + + # Testing batch creation + articles = ArticleFactory.create_batch(5) + self.assertEqual(len(articles), 5) + self.assertIsInstance(articles[0], Article) + + def test_sub_factory_nested(self) -> None: + article = ArticleFactory(author__username="admin") + + # Testing SubFactory + self.assertIsInstance(article.author, User) + self.assertIsInstance(article.author.id, int) + self.assertIsInstance(article.author.is_staff, bool) + self.assertIsInstance(article.author.date_joined, datetime) + self.assertEqual(article.author.username, "admin") + + # Testing Factory + self.assertIsInstance(article.id, int) + self.assertIsInstance(article.slug, str) + + # Testing hooks + self.assertTrue( + hasattr(article, "_pre_save_called") and article._pre_save_called + ) + self.assertTrue( + hasattr(article, "_post_save_called") and article._post_save_called + ) + self.assertTrue( + hasattr(article.author, "_pre_save_called") + and article.author._pre_save_called + ) + self.assertTrue( + hasattr(article.author, "_post_save_called") + and article.author._post_save_called + ) + + # Testing batch creation + articles = ArticleFactory.create_batch(5) + self.assertEqual(len(articles), 5) + self.assertIsInstance(articles[0], Article) + + def test_pre_save_and_post_save(self) -> None: + """Test PreSave and PostSave.""" + user = UserFactory(is_staff=True, is_active=True) + self.assertTrue( + self.client.login( + username=user.username, + password="test1234", + ) + ) + self.assertTrue(user.groups.first().name == "TestGroup1234") + + def test_group_factory(self): + group = GroupFactory() + assert group.name + + def test_user_factory(self): + user = UserFactory() + assert user.id + + def test_article_factory(self): + article = ArticleFactory() + assert article.id diff --git a/examples/django/blog/__init__.py b/examples/django/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/django/blog/asgi.py b/examples/django/blog/asgi.py new file mode 100644 index 0000000..7be97d1 --- /dev/null +++ b/examples/django/blog/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for blog project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog.settings") + +application = get_asgi_application() diff --git a/examples/django/blog/settings.py b/examples/django/blog/settings.py new file mode 100644 index 0000000..143b38d --- /dev/null +++ b/examples/django/blog/settings.py @@ -0,0 +1,171 @@ +""" +Django settings for blog project. + +Generated by 'django-admin startproject' using Django 4.2.7. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +import os +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = ( + "django-insecure-z0l(xs6*(0#0%zw*r63n-=ywho=yd%4pb$85tb8j6he)7ay0ee" +) + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "storages", + "article", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "blog.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "blog.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "django_db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", # noqa + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +MEDIA_URL = "/media/" +# STATIC_URL = "/static/" +MEDIA_ROOT = BASE_DIR / "media" +STATIC_ROOT = BASE_DIR / "static" + +# ****************** +# ****** AWS ******* +# ****************** + +# STORAGES = { +# "default": { +# "BACKEND": "storages.backends.s3.S3Storage", +# "OPTIONS": {}, +# }, +# "staticfiles": { +# "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", +# }, +# } +AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") +AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") +AWS_STORAGE_BUCKET_NAME = os.environ.get( + "AWS_STORAGE_BUCKET_NAME", "artur-testing-1" +) + +# *************************** +# ****** Google Cloud ******* +# *************************** + +# STORAGES = { +# "default": { +# "BACKEND": "storages.backends.gcloud.GoogleCloudStorage", +# "OPTIONS": {}, +# }, +# "staticfiles": { +# "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", +# }, +# } + +GOOGLE_APPLICATION_CREDENTIALS = os.environ.get( + "GOOGLE_APPLICATION_CREDENTIALS" +) +GS_BUCKET_NAME = os.environ.get("GS_BUCKET_NAME", "artur-testing-1") diff --git a/examples/django/blog/urls.py b/examples/django/blog/urls.py new file mode 100644 index 0000000..4cca360 --- /dev/null +++ b/examples/django/blog/urls.py @@ -0,0 +1,27 @@ +""" +URL configuration for blog project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path("admin/", admin.site.urls), +] + +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/examples/django/blog/wsgi.py b/examples/django/blog/wsgi.py new file mode 100644 index 0000000..9af5eed --- /dev/null +++ b/examples/django/blog/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for blog project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog.settings") + +application = get_wsgi_application() diff --git a/examples/django/manage.py b/examples/django/manage.py new file mode 100755 index 0000000..43f6f85 --- /dev/null +++ b/examples/django/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + sys.path.insert(0, os.path.abspath(os.path.join("..", ".."))) + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "blog.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/examples/django/requirements.in b/examples/django/requirements.in new file mode 100644 index 0000000..175445c --- /dev/null +++ b/examples/django/requirements.in @@ -0,0 +1,2 @@ +django +Pillow diff --git a/fakepy/django_storage/__init__.py b/fakepy/django_storage/__init__.py new file mode 100644 index 0000000..a66b2fc --- /dev/null +++ b/fakepy/django_storage/__init__.py @@ -0,0 +1,5 @@ +__title__ = "fake-py-django-storage" +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__version__ = "0.1" diff --git a/fakepy/django_storage/aws_s3.py b/fakepy/django_storage/aws_s3.py new file mode 100644 index 0000000..97d98d9 --- /dev/null +++ b/fakepy/django_storage/aws_s3.py @@ -0,0 +1,32 @@ +from botocore.exceptions import ClientError +from storages.backends.s3 import S3Storage +from storages.utils import clean_name + +from .cloud import DjangoCloudStorage + +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("DjangoAWSS3Storage",) + + +class DjangoAWSS3Storage(DjangoCloudStorage): + """Django AWS S3 storage.""" + + storage: S3Storage + + def exists(self: "DjangoAWSS3Storage", filename: str) -> bool: + """Check if file exists.""" + name = self.storage._normalize_name(clean_name(filename)) + + try: + self.storage.connection.meta.client.head_object( + Bucket=self.storage.bucket_name, Key=name + ) + return True + except ClientError as err: + if err.response["ResponseMetadata"]["HTTPStatusCode"] == 404: + return False + + # Some other error was encountered. Re-raise it. + raise diff --git a/fakepy/django_storage/azure_cloud_storage.py b/fakepy/django_storage/azure_cloud_storage.py new file mode 100644 index 0000000..9aff69e --- /dev/null +++ b/fakepy/django_storage/azure_cloud_storage.py @@ -0,0 +1,24 @@ +from storages.backends.azure_storage import AzureStorage + +from .cloud import DjangoCloudStorage + +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("DjangoAzureCloudStorage",) + + +class DjangoAzureCloudStorage(DjangoCloudStorage): + """Django AzureCloudStorage storage.""" + + storage: AzureStorage + + def exists(self: "DjangoAzureCloudStorage", filename: str) -> bool: + """Check if file exists.""" + if not filename: + return True + + blob_client = self.storage.client.get_blob_client( + self.storage._get_valid_path(filename) + ) + return blob_client.exists() diff --git a/fakepy/django_storage/base.py b/fakepy/django_storage/base.py new file mode 100644 index 0000000..35cd7c1 --- /dev/null +++ b/fakepy/django_storage/base.py @@ -0,0 +1,129 @@ +import os +from abc import abstractmethod +from pathlib import Path +from tempfile import NamedTemporaryFile +from typing import Optional, Union + +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage +from fake import BaseStorage + +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("DjangoBaseStorage",) + +DEFAULT_ROOT_PATH = "tmp" +DEFAULT_REL_PATH = "tmp" + + +class DjangoBaseStorage(BaseStorage): + """Django-based storage class using Django's default storage backend. + + Usage example: + + .. code-block:: python + + from fakepy.django_storage.filesystem import DjangoFileSystemStorage + + storage = DjangoFileSystemStorage() + docx_file = storage.generate_filename( + prefix="zzz_", extension="docx" + ) + storage.write_bytes(docx_file, b"Sample bytes data") + + Initialization with params: + + .. code-block:: python + + from fakepy.django_storage.filesystem import DjangoFileSystemStorage + + storage = DjangoFileSystemStorage() + docx_file = storage.generate_filename( + prefix="example", extension="docx" + ) + """ + + def __init__( + self: "DjangoBaseStorage", + root_path: Optional[Union[str, Path]] = DEFAULT_ROOT_PATH, + rel_path: Optional[str] = DEFAULT_REL_PATH, + *args, + **kwargs, + ) -> None: + """ + :param root_path: Root path of the storage directory. + :param rel_path: Relative path of the storage directory. + :param *args: + :param **kwargs: + """ + self.root_path = root_path or "" + self.rel_path = rel_path or "" + super().__init__(*args, **kwargs) + self.storage = default_storage + + def generate_filename( + self: "DjangoBaseStorage", + extension: str, + prefix: Optional[str] = None, + basename: Optional[str] = None, + ) -> str: + """Generate filename.""" + dir_path = os.path.join(self.root_path, self.rel_path) + + if not extension: + raise Exception("Extension shall be given!") + + if basename: + filename = f"{basename}.{extension}" + else: + temp_file = NamedTemporaryFile( + prefix=prefix, + suffix=f".{extension}", + delete=False, + ) + filename = Path(temp_file.name).name + return os.path.join(dir_path, filename) + + def write_text( + self: "DjangoBaseStorage", + filename: str, + data: str, + encoding: Optional[str] = None, + ) -> int: + """Write text.""" + if filename.startswith("/"): + filename = filename[1:] + content = ContentFile(data.encode(encoding or "utf-8")) + # saved_path = self.storage.save(filename, content) + self.storage.save(filename, content) + return len(data) + + def write_bytes( + self: "DjangoBaseStorage", + filename: str, + data: bytes, + ) -> int: + """Write bytes.""" + content = ContentFile(data) + if filename.startswith("/"): + filename = filename[1:] + # saved_path = self.storage.save(filename, content) + self.storage.save(filename, content) + return len(data) + + def exists(self: "DjangoBaseStorage", filename: str) -> bool: + """Check if file exists.""" + return self.storage.exists(filename) + + @abstractmethod + def relpath(self: "DjangoBaseStorage", filename: str) -> str: + """Return relative path.""" + + @abstractmethod + def abspath(self: "DjangoBaseStorage", filename: str) -> str: + """Return absolute path.""" + + def unlink(self: "DjangoBaseStorage", filename: str) -> None: + """Delete the file.""" + self.storage.delete(filename) diff --git a/fakepy/django_storage/cloud.py b/fakepy/django_storage/cloud.py new file mode 100644 index 0000000..860ebe8 --- /dev/null +++ b/fakepy/django_storage/cloud.py @@ -0,0 +1,17 @@ +from .base import DjangoBaseStorage + +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("DjangoCloudStorage",) + + +class DjangoCloudStorage(DjangoBaseStorage): + + def relpath(self: "DjangoCloudStorage", filename: str) -> str: + """Return relative path.""" + return filename + + def abspath(self: "DjangoCloudStorage", filename: str) -> str: + """Return absolute path.""" + return self.storage.url(filename) diff --git a/fakepy/django_storage/filesystem.py b/fakepy/django_storage/filesystem.py new file mode 100644 index 0000000..494c0ac --- /dev/null +++ b/fakepy/django_storage/filesystem.py @@ -0,0 +1,42 @@ +from .base import DjangoBaseStorage + +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("DjangoFileSystemStorage",) + + +class DjangoFileSystemStorage(DjangoBaseStorage): + """Django-based storage class using Django's default storage backend. + + Usage example: + + .. code-block:: python + + from fakepy.django_storage.filesystem import DjangoFileSystemStorage + + storage = DjangoFileSystemStorage() + docx_file = storage.generate_filename( + prefix="zzz_", extension="docx" + ) + storage.write_bytes(docx_file, b"Sample bytes data") + + Initialization with params: + + .. code-block:: python + + from fakepy.django_storage.filesystem import DjangoFileSystemStorage + + storage = DjangoFileSystemStorage() + docx_file = storage.generate_filename( + prefix="example", extension="docx" + ) + """ + + def relpath(self: "DjangoFileSystemStorage", filename: str) -> str: + """Return relative path.""" + return filename + + def abspath(self: "DjangoFileSystemStorage", filename: str) -> str: + """Return absolute path.""" + return self.storage.joinpath(filename) diff --git a/fakepy/django_storage/google_cloud_storage.py b/fakepy/django_storage/google_cloud_storage.py new file mode 100644 index 0000000..704b8c2 --- /dev/null +++ b/fakepy/django_storage/google_cloud_storage.py @@ -0,0 +1,29 @@ +from google.cloud.exceptions import NotFound +from storages.backends.gcloud import GoogleCloudStorage +from storages.utils import clean_name + +from .cloud import DjangoCloudStorage + +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("DjangoGoogleCloudStorage",) + + +class DjangoGoogleCloudStorage(DjangoCloudStorage): + """Django GoogleCloudStorage storage.""" + + storage: GoogleCloudStorage + + def exists(self: "DjangoGoogleCloudStorage", filename: str) -> bool: + """Check if file exists.""" + + if not filename: # root element aka the bucket + try: + self.storage.client.get_bucket(self.storage.bucket) + return True + except NotFound: + return False + + name = self.storage._normalize_name(clean_name(filename)) + return bool(self.storage.bucket.get_blob(name)) diff --git a/fakepy/django_storage/tests/__init__.py b/fakepy/django_storage/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fakepy/django_storage/tests/data.py b/fakepy/django_storage/tests/data.py new file mode 100644 index 0000000..4d8dab9 --- /dev/null +++ b/fakepy/django_storage/tests/data.py @@ -0,0 +1,18 @@ +__author__ = "Artur Barseghyan " +__copyright__ = "2022-2023 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("GCS_CREDENTIALS_JSON",) + +GCS_CREDENTIALS_JSON = { + "type": "service_account", + "project_id": "test-12345", + "private_key_id": "00000000000000000000000000", + "private_key": "\n-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCfx5MW2anBbHKL\n4g1iazAtyxJGoHTfR03zGHr4mVsmfe2cV5vMZY4/oO3uszpbq342tdywfkAq5O8+\nFqbUUIEfAiobs5DVvZ9iL6uAkb/erLY2ObVFDr7hKBu0oRmc2EWYH6honxb/tNFI\nzLy3Sh7Bdor4/vH7PMalgcEorxt+RkFPJeo3cGyrJfO8jv0n2u0wakHKO5bPZshZ\nBAktnYDMVPnaV+kI/ezucxow91l3BXTdPuC3zVSlSuvqRssg8/jpDRgFv/hThuVm\nhUhU8ddS76QUQ0ekj6kBPp5iYvY6sJHwU9Bmf7qG3zAx4h56WBJV+SYDVoT+VydZ\nkqMc721LAgMBAAECggEAALPfxlDzH/U6VIa/W+ujjMzNnSvld60YZais5pAq4dkN\nHqV8yBuGONECjgzE6+3g7zXbhspdvXjzDskKUMXndQ7Z+o2bZgugbZq1QvdHhB/E\nT8OIzYkkEIw2Z8ckyRcv+/XfrermyGPi+IWV96rGxqXSNKG1lLV1HJM2gAqzfaZ1\nqL/TVxj9ZjI3FRGMSuxKeRC/RG6L4iHMq+tOIKSiST6UHSci6TbDDAWXpXMWzEeM\nmHEGCvNmp5yRHEPBF5n5Dl9hEfnhp8FpW20fuANjxIFlW9KMKtu9vkGgS1AdiT9f\nE1lgy41fBdwg2rdMHn3JhCN9H+J2MK7AtoWcD5TNGQKBgQDXszOluYO85hEbtJnN\nf5XtO/aoHtnjPixiI2wcxuGmLWaAYmtgy0KwWZlQldNXM7qH1BiEZ8wTFVOfqrqY\nlDuMYxgfTqPTaESS5wkirucMdZJJXuohLgDAaOaVbL0V+1YJbnq4AnLaHB0Y8sqX\nrnCawF1ke9F0032lbkQJn3qbJQKBgQC9obvTZO/53TrKz2/NWfBDJaTey7PaZ2fg\nJwvj63nUVVM91mQ262mjoqw/Wogp6GFr46XXcyQ/xk8g32EQ1a22p1MXIkdJTic3\n/m+VF//4+GaJD1MedqOxWMVWHZyYJxjekAM/tt3/epzcDFNdab6ZlRIyAh0Ro1mZ\nqJPmbCszrwKBgQCv1F2il2JDJswFaKgDcyCVHU9I5rU436KwcS2dG6Yvn0yyFQhx\nA+Ad/zvSDAAWUo2YUZWWwUICwFzFiBfJbvRH0TOFucYj/BgCJCE3S5n+dwzDkIKM\nf4KPVjO41MYiWBpfX9bbjutuzoINp0Tsdn9GNs8qrSAl+oyuwP7nVUBNnQKBgQCT\ne2fy/vvMnoyNE0vmr942ut5BELhuUiHtqTCMMKVtyHaXD1idhfWA+JFyLFzeCwdJ\nu6FNsRUuLHN6I4EAcM9L0VLEGTrL/mZuHAp4MFQ6NCa6zhpdBPRGh73iPeF+TFoB\nLov4T6bUfW3ljgiADC/ajp+6GP62qw6SfROaD+KBrQKBgHxz/IvyRE7zORNnhcld\n9y+Qr4GPLxV9oLeJlG0gjJaYC79gQk2gUjQfT2HEQlM76SeXr0qr/jOKSZeg4h7Q\nuZib/5m3YD5bI35qBexNHz/AZ1kQ9PiHjZmgS4PiCobuuHUXRPni0Vf0FKLCouQq\n0Bb9H46yp7+n+NCkjHB3IInM\n-----END PRIVATE KEY-----\n", # noqa + "client_email": "test-12345@appspot.gserviceaccount.com", + "client_id": "2222222222222222222222222", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-12345%40appspot.gserviceaccount.com", # noqa + "universe_domain": "googleapis.com", +} diff --git a/fakepy/django_storage/tests/test_aws_s3_storage.py b/fakepy/django_storage/tests/test_aws_s3_storage.py new file mode 100644 index 0000000..c16cf07 --- /dev/null +++ b/fakepy/django_storage/tests/test_aws_s3_storage.py @@ -0,0 +1,207 @@ +from typing import Any, Dict, Type, Union + +import boto3 +from django.test import TestCase, override_settings +from fake import FAKER, FILE_REGISTRY +from moto import mock_aws +from parametrize import parametrize + +from ..aws_s3 import DjangoAWSS3Storage +from ..base import DjangoBaseStorage + +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("TestAWSS3StorageTestCase",) + + +@override_settings( + STORAGES={ + "default": { + "BACKEND": ("storages.backends.s3boto3.S3Boto3Storage"), + "OPTIONS": {"bucket_name": "test_bucket"}, + }, + } +) +@mock_aws +class TestAWSS3StorageTestCase(TestCase): + """Test AWS S3 storages.""" + + def setUp(self): + """Set up the mock S3 environment.""" + self.s3 = boto3.client("s3") + self.s3.create_bucket(Bucket="test_bucket") + + def tearDown(self) -> None: + super().tearDown() + FILE_REGISTRY.clean_up() # Clean up files + + @parametrize( + "storage_cls, kwargs, prefix, basename, extension", + [ + # DjangoAWSS3Storage + ( + DjangoAWSS3Storage, + { + "root_path": "testing", + "rel_path": "tmp", + }, + "zzz", + None, + "docx", + ), + ( + DjangoAWSS3Storage, + { + "root_path": "testing", + "rel_path": "tmp", + }, + None, + "my_zzz_filename", + "docx", + ), + ], + ) + def test_storage( + self: "TestAWSS3StorageTestCase", + storage_cls: Type[DjangoBaseStorage], + kwargs: Dict[str, Any], + prefix: Union[str, None], + basename: Union[str, None], + extension: str, + ) -> None: + """Test storage.""" + storage = storage_cls(**kwargs) + # Text file + filename_text = storage.generate_filename( + basename=basename, prefix=prefix, extension=extension + ) + # Write to the text file + text_result = storage.write_text(filename_text, "Lorem ipsum") + # Check if file exists + self.assertTrue(storage.exists(filename_text)) + # Assert correct return value + self.assertIsInstance(text_result, int) + # Clean up + storage.unlink(filename_text) + + # Bytes + filename_bytes = storage.generate_filename( + basename=basename, prefix=prefix, extension=extension + ) + # Write to bytes file + bytes_result = storage.write_bytes(filename_bytes, b"Lorem ipsum") + # Check if file exists + self.assertTrue(storage.exists(filename_bytes)) + # Assert correct return value + self.assertIsInstance(bytes_result, int) + + # Clean up + storage.unlink(filename_bytes) + + @parametrize( + "storage_cls, kwargs, prefix, extension", + [ + # DjangoAWSS3Storage + ( + DjangoAWSS3Storage, + { + "root_path": "testing", + "rel_path": "tmp", + }, + "zzz", + "", + ), + ], + ) + def test_storage_generate_filename_exceptions( + self: "TestAWSS3StorageTestCase", + storage_cls: Type[DjangoBaseStorage], + kwargs: Dict[str, Any], + prefix: str, + extension: str, + ) -> None: + """Test storage `generate_filename` exceptions.""" + storage = storage_cls(**kwargs) + + with self.assertRaises(Exception): + # Generate filename + storage.generate_filename(prefix=prefix, extension=extension) + + with self.assertRaises(Exception): + # Generate filename + storage.generate_filename(basename=prefix, extension=extension) + + @parametrize( + "storage_cls, kwargs, prefix, extension", + [ + # DjangoAWSS3Storage + ( + DjangoAWSS3Storage, + { + "root_path": "root_tmp", + "rel_path": "rel_tmp", + }, + "", + "tmp", + ), + ], + ) + def test_storage_abspath( + self: "TestAWSS3StorageTestCase", + storage_cls: Type[DjangoBaseStorage], + kwargs: Dict[str, Any], + prefix: str, + extension: str, + ) -> None: + """Test `S3Storage` `abspath`.""" + storage = storage_cls(**kwargs) + filename = storage.generate_filename( + prefix=prefix, + extension=extension, + ) + self.assertTrue(filename.startswith("root_tmp/rel_tmp/")) + + @parametrize( + "storage_cls, kwargs, prefix, extension", + [ + # DjangoAWSS3Storage + ( + DjangoAWSS3Storage, + { + "root_path": "root_tmp", + "rel_path": "rel_tmp", + }, + "", + "tmp", + ), + ], + ) + def test_storage_unlink( + self: "TestAWSS3StorageTestCase", + storage_cls: Type[DjangoBaseStorage], + kwargs: Dict[str, Any], + prefix: str, + extension: str, + ) -> None: + """Test `DjangoStorage` `unlink`.""" + storage = storage_cls(**kwargs) + with self.subTest("Test unlink by S3"): + filename_1 = storage.generate_filename( + prefix=prefix, + extension=extension, + ) + storage.write_text(filename=filename_1, data=FAKER.text()) + self.assertTrue(storage.exists(filename_1)) + storage.unlink(filename_1) + self.assertFalse(storage.exists(filename_1)) + + with self.subTest("Test unlink by str"): + filename_2 = storage.generate_filename( + prefix=prefix, + extension=extension, + ) + storage.write_text(filename=filename_2, data=FAKER.text()) + self.assertTrue(storage.exists(filename_2)) + storage.unlink(str(filename_2)) + self.assertFalse(storage.exists(filename_2)) diff --git a/fakepy/django_storage/tests/test_azure_cloud_storage.py b/fakepy/django_storage/tests/test_azure_cloud_storage.py new file mode 100644 index 0000000..e69de29 diff --git a/fakepy/django_storage/tests/test_filesystem_storage.py b/fakepy/django_storage/tests/test_filesystem_storage.py new file mode 100644 index 0000000..15d70cb --- /dev/null +++ b/fakepy/django_storage/tests/test_filesystem_storage.py @@ -0,0 +1,204 @@ +from typing import Any, Dict, Type, Union + +from django.test import TestCase, override_settings +from fake import FAKER, FILE_REGISTRY +from parametrize import parametrize + +from ..base import DjangoBaseStorage +from ..filesystem import DjangoFileSystemStorage + +__author__ = "Artur Barseghyan " +__copyright__ = "2024 Artur Barseghyan" +__license__ = "MIT" +__all__ = ("TestStoragesTestCase",) + + +@override_settings( + STORAGES={ + "default": { + "BACKEND": ( + "django.core.files.storage.filesystem.FileSystemStorage" + ), + "OPTIONS": {}, + }, + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", + }, + } +) +class TestStoragesTestCase(TestCase): + """Test storages.""" + + def tearDown(self) -> None: + super().tearDown() + FILE_REGISTRY.clean_up() # Clean up files + + @parametrize( + "storage_cls, kwargs, prefix, basename, extension", + [ + # DjangoFileSystemStorage + ( + DjangoFileSystemStorage, + { + "root_path": "testing", + "rel_path": "tmp", + }, + "zzz", + None, + "docx", + ), + ( + DjangoFileSystemStorage, + { + "root_path": "testing", + "rel_path": "tmp", + }, + None, + "my_zzz_filename", + "docx", + ), + ], + ) + def test_storage( + self: "TestStoragesTestCase", + storage_cls: Type[DjangoBaseStorage], + kwargs: Dict[str, Any], + prefix: Union[str, None], + basename: Union[str, None], + extension: str, + ) -> None: + """Test storage.""" + storage = storage_cls(**kwargs) + # Text file + filename_text = storage.generate_filename( + basename=basename, prefix=prefix, extension=extension + ) + # Write to the text file + text_result = storage.write_text(filename_text, "Lorem ipsum") + # Check if file exists + self.assertTrue(storage.exists(filename_text)) + # Assert correct return value + self.assertIsInstance(text_result, int) + # Clean up + storage.unlink(filename_text) + + # Bytes + filename_bytes = storage.generate_filename( + basename=basename, prefix=prefix, extension=extension + ) + # Write to bytes file + bytes_result = storage.write_bytes(filename_bytes, b"Lorem ipsum") + # Check if file exists + self.assertTrue(storage.exists(filename_bytes)) + # Assert correct return value + self.assertIsInstance(bytes_result, int) + + # Clean up + storage.unlink(filename_bytes) + + @parametrize( + "storage_cls, kwargs, prefix, extension", + [ + # DjangoFileSystemStorage + ( + DjangoFileSystemStorage, + { + "root_path": "testing", + "rel_path": "tmp", + }, + "zzz", + "", + ), + ], + ) + def test_storage_generate_filename_exceptions( + self: "TestStoragesTestCase", + storage_cls: Type[DjangoBaseStorage], + kwargs: Dict[str, Any], + prefix: str, + extension: str, + ) -> None: + """Test storage `generate_filename` exceptions.""" + storage = storage_cls(**kwargs) + + with self.assertRaises(Exception): + # Generate filename + storage.generate_filename(prefix=prefix, extension=extension) + + with self.assertRaises(Exception): + # Generate filename + storage.generate_filename(basename=prefix, extension=extension) + + @parametrize( + "storage_cls, kwargs, prefix, extension", + [ + # DjangoFileSystemStorage + ( + DjangoFileSystemStorage, + { + "root_path": "root_tmp", + "rel_path": "rel_tmp", + }, + "", + "tmp", + ), + ], + ) + def test_storage_abspath( + self: "TestStoragesTestCase", + storage_cls: Type[DjangoBaseStorage], + kwargs: Dict[str, Any], + prefix: str, + extension: str, + ) -> None: + """Test `FileSystemStorage` `abspath`.""" + storage = storage_cls(**kwargs) + filename = storage.generate_filename( + prefix=prefix, + extension=extension, + ) + self.assertTrue(filename.startswith("root_tmp/rel_tmp/")) + + @parametrize( + "storage_cls, kwargs, prefix, extension", + [ + # DjangoFileSystemStorage + ( + DjangoFileSystemStorage, + { + "root_path": "root_tmp", + "rel_path": "rel_tmp", + }, + "", + "tmp", + ), + ], + ) + def test_storage_unlink( + self: "TestStoragesTestCase", + storage_cls: Type[DjangoBaseStorage], + kwargs: Dict[str, Any], + prefix: str, + extension: str, + ) -> None: + """Test `DjangoStorage` `unlink`.""" + storage = storage_cls(**kwargs) + with self.subTest("Test unlink by Django"): + filename_1 = storage.generate_filename( + prefix=prefix, + extension=extension, + ) + storage.write_text(filename=filename_1, data=FAKER.text()) + self.assertTrue(storage.exists(filename_1)) + storage.unlink(filename_1) + self.assertFalse(storage.exists(filename_1)) + + with self.subTest("Test unlink by str"): + filename_2 = storage.generate_filename( + prefix=prefix, + extension=extension, + ) + storage.write_text(filename=filename_2, data=FAKER.text()) + self.assertTrue(storage.exists(filename_2)) + storage.unlink(str(filename_2)) + self.assertFalse(storage.exists(filename_2)) diff --git a/fakepy/django_storage/tests/test_google_cloud_storage.py b/fakepy/django_storage/tests/test_google_cloud_storage.py new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1c32014 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,226 @@ +[project] +name = "fake-py-django-storage" +description = "Django storage for fake.py" +readme = "README.rst" +version = "0.1" +dependencies = [ + "fake.py", + "django", +] +authors = [ + {name = "Artur Barseghyan", email = "artur.barseghyan@gmail.com"}, +] +maintainers = [ + {name = "Artur Barseghyan", email = "artur.barseghyan@gmail.com"}, +] +license = {text = "MIT"} +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python", + "Topic :: Software Development :: Testing", + "Topic :: Software Development", +] +keywords = ["fake data", "test data"] + +[project.urls] +Homepage = "https://github.com/barseghyanartur/fake-py-django-storage/" +Issues = "https://github.com/barseghyanartur/fake-py-django-storage/issues" +Documentation = "https://fake-py-django-storage.readthedocs.io/" +Repository = "https://github.com/barseghyanartur/fake-py-django-storage/" +Changelog = "https://fake-py-django-storage.readthedocs.io/en/latest/changelog.html" + +[project.optional-dependencies] +all = ["fake-py-django-storage[gcs,s3,azure,dev,test,docs]"] +dev = [ + "black", + "detect-secrets", + "doc8", + "ipython", + "isort", + "mypy", + "pip-tools", + "pydoclint", + "ruff", + "twine", + "uv", +] +test = [ + "django", + "parametrize", + "pytest", + "pytest-cov", + "pytest-django", + "pytest-rst", +] +docs = [ + "sphinx<6.0", + "sphinx-rtd-theme>=1.3.0", + "sphinx-no-pragma", +] +gcs = [ + "django-storages[google]", +] +s3 = [ + "django-storages[s3]", +] +azure = [ + "django-storages[azure]", +] + +[project.scripts] + +[tool.setuptools.packages.find] +where = ["."] +include = ["fakepy.django_storage"] +namespaces = true + +# Build system information below. +[build-system] +requires = ["setuptools>=41.0", "setuptools-scm", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 80 +target-version = ['py38', 'py39', 'py310', 'py311'] +include = '\.pyi?$' +force-exclude = '__copy_fake\.py' +extend-exclude = ''' +/( + # The following are specific to Black, you probably don't want those. + | blib2to3 + | tests/data + | profiling + | migrations +)/ +''' + +[tool.isort] +profile = "black" +combine_as_imports = true +multi_line_output = 3 +force_single_line = false +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 80 +honor_noqa = true +known_first_party = [ + "address", + "article", + "config", + "data", + "fake_address", + "fake_band", +] +known_third_party = ["fake", "fakepy"] +skip = ["wsgi.py", "builddocs/"] + +[tool.ruff] +line-length = 80 + +# Enable Pyflakes `E` and `F` codes by default. +lint.select = ["E", "F"] +lint.ignore = [] + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "docs", +] +lint.per-file-ignores = {} + +# Allow unused variables when underscore-prefixed. +lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +# Assume Python 3.8. +target-version = "py38" + +[tool.doc8] +ignore-path = [ + "docs/requirements.txt", + "fake-py-django-storage.egg-info/SOURCES.txt", +] + +[tool.pytest.ini_options] +addopts = [ + "-ra", + "-vvv", + "-q", + "--cov=django_storage", + "--ignore=.tox", + "--ignore=requirements", + "--ignore=release", + "--ignore=tmp", + "--cov-report=html", + "--cov-report=term", + "--cov-report=annotate", + "--cov-append", + "--capture=no", +] +testpaths = [ + "fakepy/django_storage/", + "tests.py", + "test_*.py", + "*.rst", + "**/*.rst", + "docs/*.rst", +] +pythonpath = [ + ".", + "fakepy/django_storage/", + "examples/django", +] +DJANGO_SETTINGS_MODULE = "blog.settings" + +[tool.coverage.run] +relative_files = true +omit = [ + ".tox/*", +] + +[tool.coverage.report] +show_missing = true +exclude_lines = [ + "pragma: no cover", + "@overload", +] + +[tool.mypy] +check_untyped_defs = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unused_configs = true + +[tool.pydoclint] +style = 'sphinx' +exclude = '\.git|\.tox|tests/data|__copy_fake\.py' +require-return-section-when-returning-nothing = false +allow-init-docstring = true +arg-type-hints-in-docstring = false