django-syzygy


Namedjango-syzygy JSON
Version 1.1.0 PyPI version JSON
download
home_pagehttps://github.com/charettes/django-syzygy
SummaryDeployment aware tooling for Django migrations.
upload_time2024-05-24 23:32:06
maintainerNone
docs_urlNone
authorSimon Charette
requires_pythonNone
licenseMIT License
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            django-syzygy
=============

.. image:: https://github.com/charettes/django-syzygy/workflows/Test/badge.svg
    :target: https://github.com/charettes/django-syzygy/actions
    :alt: Build Status

.. image:: https://coveralls.io/repos/github/charettes/django-syzygy/badge.svg?branch=master
    :target: https://coveralls.io/github/charettes/django-syzygy?branch=master
    :alt: Coverage status


Django application providing database migration tooling to automate their deployment.

Inspired by a `2015 post from Ludwig Hähne`_ and experience dealing with migration at Zapier_.

.. _`2015 post from Ludwig Hähne`: https://pankrat.github.io/2015/django-migrations-without-downtimes/#django-wishlist
.. _Zapier: https://zapier.com

Currently only supports PostgreSQL and SQLite as they are the only two FOSS
core backends that support transactional DDL and this tool is built around
that expectation.

Installation
------------

.. code:: sh

    pip install django-syzygy

Usage
-----

Add ``'syzygy'`` to your ``INSTALLED_APPS``

.. code:: python

    # settings.py
    INSTALLED_APPS = [
        ...
        'syzygy',
        ...
    ]

Setup you deployment pipeline to run ``migrate --pre-deploy`` before rolling
out your code changes and ``migrate`` afterwards to apply the postponed
migrations.

Concept
-------

When dealing with database migrations in the context of an highly available
application managed through continuous deployment the Django migration
leaves a lot to be desired in terms of the sequencing of operations it
generates.

The automatically generated schema alterations for field additions, removals,
renames, and others do not account for deployments where versions of the old
and the new code must co-exist for a short period of time.

For example, adding a field with a ``default`` does not persist a database
level default which prevents ``INSERT`` from the pre-existing code which
ignores the existence of tentatively added field from succeeding.

Figuring out the proper sequencing of operations is doable but non-trivial and
error prone. Syzygy ought to provide a solution to this problem by introducing
a notion of *prerequisite* and *postponed* migrations with regards to
deployment and generating migrations that are aware of this sequencing.

A migration is assumed to be a *prerequisite* to deployment unless it contains
a destructive operation or the migration has its ``stage`` class attribute set
to ``Stage.POST_DEPLOY``. When this attribute is defined it will bypass
``operations`` based heuristics.

e.g. this migration would be considered a *prerequisite*

.. code:: python

    class Migration(migrations.Migration):
        operations = [
            AddField('model', 'field', models.IntegerField(null=True))
        ]

while the following migrations would be *postponed*

.. code:: python

    class Migration(migrations.Migration):
        operations = [
            RemoveField('model', 'field'),
        ]

.. code:: python

    from syzygy import Stage

    class Migration(migrations.Migration):
        stage = Stage.POST_DEPLOY

        operations = [
            RunSQL(...),
        ]

To take advantage of this new notion of migration stage the `migrate` command
allows migrations meant to be run before a deployment to be targeted using
`--pre-deploy` flag.

What it does and doesn't do
---------------------------

It does
^^^^^^^
- Introduce a notion of pre and post-deployment migrations and support their
  creation, management, and deployment sequencing through adjustments made to
  the ``makemigrations`` and ``migrate`` command.
- Automatically split operations known to cause deployment sequencing issues
  in pre and post deployment stages.
- Refuse the temptation to guess in the face of ambiguity and force developers
  to reflect about the sequencing of their operations when dealing with
  non-trival changes. It is meant to provide guardrails with safe quality of
  life defaults.

It doesn't
^^^^^^^^^^
- Generate operations that are guaranteed to minimize contention on your
  database. You should investigate the usage of `database specific solutions`_
  for that.
- Allow developers to completely abstract the notion of sequencing of
  of operations. There are changes that are inherently unsafe or not deployable
  in an atomic manner and you should be prepared to deal with them.

.. _`database specific solutions`: https://pypi.org/project/django-pg-zero-downtime-migrations/

Specialized operations
----------------------

Syzygy overrides the ``makemigrations`` command to automatically split
and organize operations in a way that allows them to safely be applied
in pre and post-deployment stages. 

Field addition
^^^^^^^^^^^^^^

When adding a field to an existing model Django will generate an
``AddField`` operation that roughly translates to the following SQL

.. code:: sql

    ALTER TABLE "author" ADD COLUMN "dob" int NOT NULL DEFAULT 1988;
    ALTER TABLE "author" ALTER COLUMN "dob" DROP DEFAULT;

Which isn't safe as the immediate removal of the database level ``DEFAULT``
prevents the code deployed at the time of migration application from inserting
new records.

In order to make this change safe syzygy splits the operation in two, a
specialized ``AddField`` operation that performs the column addition without
the ``DROP DEFAULT`` and follow up ``PostAddField`` operation that drops the
database level default. The first is marked as ``Stage.PRE_DEPLOY`` and the
second as ``Stage.POST_DEPLOY``.

.. note::

    On Django 5.0+ the specialized operations are respectively replaced by
    vanilla ``AddField`` and ``AlterField`` ones that make use of the newly
    introduced support for ``db_default`` feature.

Field removal
^^^^^^^^^^^^^

When removing a field from an existing model Django will generate a
``RemoveField`` operation that roughly translates to the following SQL

.. code:: sql

    ALTER TABLE "author" DROP COLUMN "dob";

Such operation cannot be run before deployment because it would cause
any ``SELECT``, ``INSERT``, and ``UPDATE`` initiated by the pre-existing code
to crash while doing it after deployment would cause ``INSERT`` crashes in the
newly-deployed code that _forgot_ the existence of the field.

In order to make this change safe syzygy splits the operation in two, a
specialized ``PreRemoveField`` operation adds a database level ``DEFAULT`` to
the column if a ``Field.default`` is present or make the field nullable
otherwise and a second vanilla ``RemoveField`` operation. The first is marked as
``Stage.PRE_DEPLOY`` and the second as ``Stage.POST_DEPLOY`` just like any
``RemoveField``.

The presence of a database level ``DEFAULT`` or the removal of the ``NOT NULL``
constraint ensures a smooth rollout sequence.

.. note::

    On Django 5.0+ the specialized ``PreRemoveField`` operation is replaced by
    a vanilla ``AlterField`` that make use of the newly introduced support for
    ``db_default`` feature.

Checks
------

In order to prevent the creation of migrations mixing operations of different
*stages* this package registers `system checks`_. These checks will generate an error
for every migration with an ambiguous ``stage``.

e.g. a migration mixing inferred stages would result in a check error

.. code:: python

    class Migration(migrations.Migration):
        operations = [
            AddField('model', 'other_field', models.IntegerField(null=True)),
            RemoveField('model', 'field'),
        ]

By default, syzygy should *not* generate automatically migrations and you should
only run into check failures when manually creating migrations or adding syzygy
to an historical project.

For migrations that are part of your project and trigger a failure of this check
it is recommended to manually annotate them with proper ``stage: syzygy.stageStage``
annotations. For third party migrations you should refer to the following section.

.. _`system checks`: https://docs.djangoproject.com/en/stable/topics/checks/

Third-party migrations
----------------------

As long as the adoption of migration stages concept is not generalized your
project might depend on third-party apps containing migrations with an
ambiguous sequence of operations.

Since an explicit ``stage`` cannot be explicitly assigned by editing these
migrations a fallback or an override stage can be specified through the
respective ``MIGRATION_STAGES_FALLBACK`` and ``MIGRATION_STAGES_OVERRIDE``
settings.

By default third-party app migrations with an ambiguous sequence of operations
will fallback to ``Stage.PRE_DEPLOY`` but this behavior can be changed by
setting ``MIGRATION_THIRD_PARTY_STAGES_FALLBACK`` to ``Stage.POST_DEPLOY`` or
disabled by setting it to ``None``.

.. note::

  The third-party app detection logic relies on the ``site`` `Python module`_
  and is known to not properly detect all kind of third-party Django
  applications. You should rely on ``MIGRATION_STAGES_FALLBACK`` and
  ``MIGRATION_STAGES_OVERRIDE`` to configure stages if it doesn't work for your
  setup.

.. _`Python module`: https://docs.python.org/3/library/site.html

Reverts
-------

Migration revert are also supported and result in inverting the nature of
migrations. A migration that is normally considered a *prerequisite* would then
be *postponed* when reverted.

CI Integration
--------------

In order to ensure that no feature branch includes an ambiguous sequence of
operations users are encouraged to include a job that attempts to run the
``migrate --pre-deploy`` command against a database that only includes the
changes from the target branch.

For example, given a feature branch ``add-shiny-feature`` and a target branch
of ``main`` a script would look like

.. code:: sh

    git checkout main
    python manage.py migrate
    git checkout add-shiny-feature
    python manage.py migrate --pre-deploy

Assuming the feature branch contains a sequence of operations that cannot be
applied in a single atomic deployment consisting of pre-deployment, deployment,
and post-deployment stages the ``migrate --pre-deploy`` command will fail with
an ``AmbiguousPlan`` exception detailing the ambiguity and resolution paths.

Migration quorum
----------------

When deploying migrations to multiple clusters sharing the same database it's
important that:

1. Migrations are applied only once
2. Pre-deployment migrations are applied before deployment in any clusters is
   takes place
3. Post-deployment migrations are only applied once all clusters are done
   deploying

The built-in ``migrate`` command doesn't offer any guarantees with regards to
serializability of invocations, in other words naively calling ``migrate`` from
multiple clusters before or after a deployment could cause some migrations to
be attempted to be applied twice.

To circumvent this limitation Syzygy introduces a ``--quorum <N:int>`` flag to the
``migrate`` command that allow clusters coordination to take place.

When specified the ``migrate --quorum <N:int>`` command will wait for at least
``N`` number invocations of ``migrate`` for the planned migrations before proceeding
with applying them once and blocking on all callers until the operation completes.

In order to use the ``--quorum`` feature you must configure the ``MIGRATION_QUORUM_BACKEND``
setting to point to a quorum backend such as cache based one provided by Sygyzy

.. code:: python

    MIGRATION_QUORUM_BACKEND = 'syzygy.quorum.backends.cache.CacheQuorum'

or

.. code:: python

    CACHES = {
        ...,
        'quorum': {
            ...
        },
    }
    MIGRATION_QUORUM_BACKEND = {
        'backend': 'syzygy.quorum.backends.cache.CacheQuorum',
        'alias': 'quorum',
    }

.. note::

  In order for ``CacheQuorum`` to work properly in a distributed environment it
  must be pointed at a backend that supports atomic ``incr`` operations such as
  Memcached or Redis.


Development
-----------

Make your changes, and then run tests via tox:

.. code:: sh

    tox

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/charettes/django-syzygy",
    "name": "django-syzygy",
    "maintainer": null,
    "docs_url": null,
    "requires_python": null,
    "maintainer_email": null,
    "keywords": null,
    "author": "Simon Charette",
    "author_email": "charette.s@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/f1/d5/52d41950e85df1fa34b7a28e576beebe3d1cea8cab7cf739004ce5aede57/django_syzygy-1.1.0.tar.gz",
    "platform": null,
    "description": "django-syzygy\n=============\n\n.. image:: https://github.com/charettes/django-syzygy/workflows/Test/badge.svg\n    :target: https://github.com/charettes/django-syzygy/actions\n    :alt: Build Status\n\n.. image:: https://coveralls.io/repos/github/charettes/django-syzygy/badge.svg?branch=master\n    :target: https://coveralls.io/github/charettes/django-syzygy?branch=master\n    :alt: Coverage status\n\n\nDjango application providing database migration tooling to automate their deployment.\n\nInspired by a `2015 post from Ludwig H\u00e4hne`_ and experience dealing with migration at Zapier_.\n\n.. _`2015 post from Ludwig H\u00e4hne`: https://pankrat.github.io/2015/django-migrations-without-downtimes/#django-wishlist\n.. _Zapier: https://zapier.com\n\nCurrently only supports PostgreSQL and SQLite as they are the only two FOSS\ncore backends that support transactional DDL and this tool is built around\nthat expectation.\n\nInstallation\n------------\n\n.. code:: sh\n\n    pip install django-syzygy\n\nUsage\n-----\n\nAdd ``'syzygy'`` to your ``INSTALLED_APPS``\n\n.. code:: python\n\n    # settings.py\n    INSTALLED_APPS = [\n        ...\n        'syzygy',\n        ...\n    ]\n\nSetup you deployment pipeline to run ``migrate --pre-deploy`` before rolling\nout your code changes and ``migrate`` afterwards to apply the postponed\nmigrations.\n\nConcept\n-------\n\nWhen dealing with database migrations in the context of an highly available\napplication managed through continuous deployment the Django migration\nleaves a lot to be desired in terms of the sequencing of operations it\ngenerates.\n\nThe automatically generated schema alterations for field additions, removals,\nrenames, and others do not account for deployments where versions of the old\nand the new code must co-exist for a short period of time.\n\nFor example, adding a field with a ``default`` does not persist a database\nlevel default which prevents ``INSERT`` from the pre-existing code which\nignores the existence of tentatively added field from succeeding.\n\nFiguring out the proper sequencing of operations is doable but non-trivial and\nerror prone. Syzygy ought to provide a solution to this problem by introducing\na notion of *prerequisite* and *postponed* migrations with regards to\ndeployment and generating migrations that are aware of this sequencing.\n\nA migration is assumed to be a *prerequisite* to deployment unless it contains\na destructive operation or the migration has its ``stage`` class attribute set\nto ``Stage.POST_DEPLOY``. When this attribute is defined it will bypass\n``operations`` based heuristics.\n\ne.g. this migration would be considered a *prerequisite*\n\n.. code:: python\n\n    class Migration(migrations.Migration):\n        operations = [\n            AddField('model', 'field', models.IntegerField(null=True))\n        ]\n\nwhile the following migrations would be *postponed*\n\n.. code:: python\n\n    class Migration(migrations.Migration):\n        operations = [\n            RemoveField('model', 'field'),\n        ]\n\n.. code:: python\n\n    from syzygy import Stage\n\n    class Migration(migrations.Migration):\n        stage = Stage.POST_DEPLOY\n\n        operations = [\n            RunSQL(...),\n        ]\n\nTo take advantage of this new notion of migration stage the `migrate` command\nallows migrations meant to be run before a deployment to be targeted using\n`--pre-deploy` flag.\n\nWhat it does and doesn't do\n---------------------------\n\nIt does\n^^^^^^^\n- Introduce a notion of pre and post-deployment migrations and support their\n  creation, management, and deployment sequencing through adjustments made to\n  the ``makemigrations`` and ``migrate`` command.\n- Automatically split operations known to cause deployment sequencing issues\n  in pre and post deployment stages.\n- Refuse the temptation to guess in the face of ambiguity and force developers\n  to reflect about the sequencing of their operations when dealing with\n  non-trival changes. It is meant to provide guardrails with safe quality of\n  life defaults.\n\nIt doesn't\n^^^^^^^^^^\n- Generate operations that are guaranteed to minimize contention on your\n  database. You should investigate the usage of `database specific solutions`_\n  for that.\n- Allow developers to completely abstract the notion of sequencing of\n  of operations. There are changes that are inherently unsafe or not deployable\n  in an atomic manner and you should be prepared to deal with them.\n\n.. _`database specific solutions`: https://pypi.org/project/django-pg-zero-downtime-migrations/\n\nSpecialized operations\n----------------------\n\nSyzygy overrides the ``makemigrations`` command to automatically split\nand organize operations in a way that allows them to safely be applied\nin pre and post-deployment stages. \n\nField addition\n^^^^^^^^^^^^^^\n\nWhen adding a field to an existing model Django will generate an\n``AddField`` operation that roughly translates to the following SQL\n\n.. code:: sql\n\n    ALTER TABLE \"author\" ADD COLUMN \"dob\" int NOT NULL DEFAULT 1988;\n    ALTER TABLE \"author\" ALTER COLUMN \"dob\" DROP DEFAULT;\n\nWhich isn't safe as the immediate removal of the database level ``DEFAULT``\nprevents the code deployed at the time of migration application from inserting\nnew records.\n\nIn order to make this change safe syzygy splits the operation in two, a\nspecialized ``AddField`` operation that performs the column addition without\nthe ``DROP DEFAULT`` and follow up ``PostAddField`` operation that drops the\ndatabase level default. The first is marked as ``Stage.PRE_DEPLOY`` and the\nsecond as ``Stage.POST_DEPLOY``.\n\n.. note::\n\n    On Django 5.0+ the specialized operations are respectively replaced by\n    vanilla ``AddField`` and ``AlterField`` ones that make use of the newly\n    introduced support for ``db_default`` feature.\n\nField removal\n^^^^^^^^^^^^^\n\nWhen removing a field from an existing model Django will generate a\n``RemoveField`` operation that roughly translates to the following SQL\n\n.. code:: sql\n\n    ALTER TABLE \"author\" DROP COLUMN \"dob\";\n\nSuch operation cannot be run before deployment because it would cause\nany ``SELECT``, ``INSERT``, and ``UPDATE`` initiated by the pre-existing code\nto crash while doing it after deployment would cause ``INSERT`` crashes in the\nnewly-deployed code that _forgot_ the existence of the field.\n\nIn order to make this change safe syzygy splits the operation in two, a\nspecialized ``PreRemoveField`` operation adds a database level ``DEFAULT`` to\nthe column if a ``Field.default`` is present or make the field nullable\notherwise and a second vanilla ``RemoveField`` operation. The first is marked as\n``Stage.PRE_DEPLOY`` and the second as ``Stage.POST_DEPLOY`` just like any\n``RemoveField``.\n\nThe presence of a database level ``DEFAULT`` or the removal of the ``NOT NULL``\nconstraint ensures a smooth rollout sequence.\n\n.. note::\n\n    On Django 5.0+ the specialized ``PreRemoveField`` operation is replaced by\n    a vanilla ``AlterField`` that make use of the newly introduced support for\n    ``db_default`` feature.\n\nChecks\n------\n\nIn order to prevent the creation of migrations mixing operations of different\n*stages* this package registers `system checks`_. These checks will generate an error\nfor every migration with an ambiguous ``stage``.\n\ne.g. a migration mixing inferred stages would result in a check error\n\n.. code:: python\n\n    class Migration(migrations.Migration):\n        operations = [\n            AddField('model', 'other_field', models.IntegerField(null=True)),\n            RemoveField('model', 'field'),\n        ]\n\nBy default, syzygy should *not* generate automatically migrations and you should\nonly run into check failures when manually creating migrations or adding syzygy\nto an historical project.\n\nFor migrations that are part of your project and trigger a failure of this check\nit is recommended to manually annotate them with proper ``stage: syzygy.stageStage``\nannotations. For third party migrations you should refer to the following section.\n\n.. _`system checks`: https://docs.djangoproject.com/en/stable/topics/checks/\n\nThird-party migrations\n----------------------\n\nAs long as the adoption of migration stages concept is not generalized your\nproject might depend on third-party apps containing migrations with an\nambiguous sequence of operations.\n\nSince an explicit ``stage`` cannot be explicitly assigned by editing these\nmigrations a fallback or an override stage can be specified through the\nrespective ``MIGRATION_STAGES_FALLBACK`` and ``MIGRATION_STAGES_OVERRIDE``\nsettings.\n\nBy default third-party app migrations with an ambiguous sequence of operations\nwill fallback to ``Stage.PRE_DEPLOY`` but this behavior can be changed by\nsetting ``MIGRATION_THIRD_PARTY_STAGES_FALLBACK`` to ``Stage.POST_DEPLOY`` or\ndisabled by setting it to ``None``.\n\n.. note::\n\n  The third-party app detection logic relies on the ``site`` `Python module`_\n  and is known to not properly detect all kind of third-party Django\n  applications. You should rely on ``MIGRATION_STAGES_FALLBACK`` and\n  ``MIGRATION_STAGES_OVERRIDE`` to configure stages if it doesn't work for your\n  setup.\n\n.. _`Python module`: https://docs.python.org/3/library/site.html\n\nReverts\n-------\n\nMigration revert are also supported and result in inverting the nature of\nmigrations. A migration that is normally considered a *prerequisite* would then\nbe *postponed* when reverted.\n\nCI Integration\n--------------\n\nIn order to ensure that no feature branch includes an ambiguous sequence of\noperations users are encouraged to include a job that attempts to run the\n``migrate --pre-deploy`` command against a database that only includes the\nchanges from the target branch.\n\nFor example, given a feature branch ``add-shiny-feature`` and a target branch\nof ``main`` a script would look like\n\n.. code:: sh\n\n    git checkout main\n    python manage.py migrate\n    git checkout add-shiny-feature\n    python manage.py migrate --pre-deploy\n\nAssuming the feature branch contains a sequence of operations that cannot be\napplied in a single atomic deployment consisting of pre-deployment, deployment,\nand post-deployment stages the ``migrate --pre-deploy`` command will fail with\nan ``AmbiguousPlan`` exception detailing the ambiguity and resolution paths.\n\nMigration quorum\n----------------\n\nWhen deploying migrations to multiple clusters sharing the same database it's\nimportant that:\n\n1. Migrations are applied only once\n2. Pre-deployment migrations are applied before deployment in any clusters is\n   takes place\n3. Post-deployment migrations are only applied once all clusters are done\n   deploying\n\nThe built-in ``migrate`` command doesn't offer any guarantees with regards to\nserializability of invocations, in other words naively calling ``migrate`` from\nmultiple clusters before or after a deployment could cause some migrations to\nbe attempted to be applied twice.\n\nTo circumvent this limitation Syzygy introduces a ``--quorum <N:int>`` flag to the\n``migrate`` command that allow clusters coordination to take place.\n\nWhen specified the ``migrate --quorum <N:int>`` command will wait for at least\n``N`` number invocations of ``migrate`` for the planned migrations before proceeding\nwith applying them once and blocking on all callers until the operation completes.\n\nIn order to use the ``--quorum`` feature you must configure the ``MIGRATION_QUORUM_BACKEND``\nsetting to point to a quorum backend such as cache based one provided by Sygyzy\n\n.. code:: python\n\n    MIGRATION_QUORUM_BACKEND = 'syzygy.quorum.backends.cache.CacheQuorum'\n\nor\n\n.. code:: python\n\n    CACHES = {\n        ...,\n        'quorum': {\n            ...\n        },\n    }\n    MIGRATION_QUORUM_BACKEND = {\n        'backend': 'syzygy.quorum.backends.cache.CacheQuorum',\n        'alias': 'quorum',\n    }\n\n.. note::\n\n  In order for ``CacheQuorum`` to work properly in a distributed environment it\n  must be pointed at a backend that supports atomic ``incr`` operations such as\n  Memcached or Redis.\n\n\nDevelopment\n-----------\n\nMake your changes, and then run tests via tox:\n\n.. code:: sh\n\n    tox\n",
    "bugtrack_url": null,
    "license": "MIT License",
    "summary": "Deployment aware tooling for Django migrations.",
    "version": "1.1.0",
    "project_urls": {
        "Homepage": "https://github.com/charettes/django-syzygy"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "db8f39287b43dac283dba5666cf19fdef5ad806084288e09ca555bc984e6054b",
                "md5": "9c02d8182c3d6eb3030b51748ae98d39",
                "sha256": "4c5a26b77467b41cee3c8dcaac5b0dc6b4de0843921b9f619dc04d02de5c43aa"
            },
            "downloads": -1,
            "filename": "django_syzygy-1.1.0-py2.py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "9c02d8182c3d6eb3030b51748ae98d39",
            "packagetype": "bdist_wheel",
            "python_version": "py2.py3",
            "requires_python": null,
            "size": 25178,
            "upload_time": "2024-05-24T23:32:04",
            "upload_time_iso_8601": "2024-05-24T23:32:04.869465Z",
            "url": "https://files.pythonhosted.org/packages/db/8f/39287b43dac283dba5666cf19fdef5ad806084288e09ca555bc984e6054b/django_syzygy-1.1.0-py2.py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "f1d552d41950e85df1fa34b7a28e576beebe3d1cea8cab7cf739004ce5aede57",
                "md5": "5388bae4316d04e17e4306ef3c907f50",
                "sha256": "999120fd159a4ec7600c914a85e99321121ceaf2d0199e480c0fdcbf56c17436"
            },
            "downloads": -1,
            "filename": "django_syzygy-1.1.0.tar.gz",
            "has_sig": false,
            "md5_digest": "5388bae4316d04e17e4306ef3c907f50",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 30140,
            "upload_time": "2024-05-24T23:32:06",
            "upload_time_iso_8601": "2024-05-24T23:32:06.640931Z",
            "url": "https://files.pythonhosted.org/packages/f1/d5/52d41950e85df1fa34b7a28e576beebe3d1cea8cab7cf739004ce5aede57/django_syzygy-1.1.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-05-24 23:32:06",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "charettes",
    "github_project": "django-syzygy",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "django-syzygy"
}
        
Elapsed time: 0.24403s