scim2-filter-parser


Namescim2-filter-parser JSON
Version 0.7.0 PyPI version JSON
download
home_pagehttps://pypi.org/project/scim2-filter-parser/
SummaryA customizable parser/transpiler for SCIM2.0 filters.
upload_time2024-07-20 16:38:23
maintainerPaul Logston
docs_urlNone
authorPaul Logston
requires_python>=3.8
licenseMIT
keywords scim scim2 2.0 filter
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage
            SCIM 2.0 Filter Parser
======================

|github| |codecov| |docs|

.. |codecov| image:: https://codecov.io/gh/15five/scim2-filter-parser/branch/master/graph/badge.svg
  :target: https://codecov.io/gh/15five/scim2-filter-parser

.. |docs| image:: https://readthedocs.org/projects/scim2-filter-parser/badge/?version=latest
  :target: https://scim2-filter-parser.readthedocs.io/en/latest/?badge=latest
  :alt: Documentation Status

.. |github| image:: https://github.com/15five/scim2-filter-parser/workflows/CI%2FCD/badge.svg
  :target: https://github.com/15five/scim2-filter-parser/actions?workflow=CI%2FCD
  :alt: CI/CD Status

Description
-----------

SCIM 2.0 defines queries that look like this::

    'emails[type eq "work" and value co "@example.com"] or ims[type eq "xmpp" and value co "@foo.com"]'

These can be hard to work with and covert into SQL to run against a database.

That's where SCIM 2.0 Filter Parser (SFP) can help.

SFP is broken up into four modules, each handling a different part of
translating a SCIM call into a SQL query.

The first step is tokenization or lexical analysis where the filter query
is broken down into many tokens that make it up.

::

    sfp-lexer 'emails[type eq "work" and value co "@example.com"] or ims[type eq "xmpp" and value co "@foo.com"]'

    Token(type='ATTRNAME', value='emails', lineno=1, index=0)
    Token(type='LBRACKET', value='[', lineno=1, index=6)
    Token(type='ATTRNAME', value='type', lineno=1, index=7)
    Token(type='EQ', value='eq', lineno=1, index=12)
    Token(type='COMP_VALUE', value='work', lineno=1, index=15)
    Token(type='AND', value='and', lineno=1, index=22)
    Token(type='ATTRNAME', value='value', lineno=1, index=26)
    Token(type='CO', value='co', lineno=1, index=32)
    Token(type='COMP_VALUE', value='@example.com', lineno=1, index=35)
    Token(type='RBRACKET', value=']', lineno=1, index=49)
    Token(type='OR', value='or', lineno=1, index=51)
    Token(type='ATTRNAME', value='ims', lineno=1, index=54)
    Token(type='LBRACKET', value='[', lineno=1, index=57)
    Token(type='ATTRNAME', value='type', lineno=1, index=58)
    Token(type='EQ', value='eq', lineno=1, index=63)
    Token(type='COMP_VALUE', value='xmpp', lineno=1, index=66)
    Token(type='AND', value='and', lineno=1, index=73)
    Token(type='ATTRNAME', value='value', lineno=1, index=77)
    Token(type='CO', value='co', lineno=1, index=83)
    Token(type='COMP_VALUE', value='@foo.com', lineno=1, index=86)
    Token(type='RBRACKET', value=']', lineno=1, index=96)


The second step is to convert that series of tokens into a abstract syntax tree.

::

    sfp-parser 'emails[type eq "work" and value co "@example.com"] or ims[type eq "xmpp" and value co "@foo.com"]'

    Filter(expr=LogExpr, negated=False, namespace=None)
        LogExpr(op='or', expr1=Filter, expr2=Filter)
            Filter(expr=Filter, negated=False, namespace=None)
                Filter(expr=Filter, negated=False, namespace=AttrPath)
                    Filter(expr=LogExpr, negated=False, namespace=None)
                        LogExpr(op='and', expr1=Filter, expr2=Filter)
                            Filter(expr=AttrExpr, negated=False, namespace=None)
                                AttrExpr(value='eq', attr_path=AttrPath, comp_value=CompValue)
                                    AttrPath(attr_name='type', sub_attr=None, uri=None)
                                    CompValue(value='work')
                            Filter(expr=AttrExpr, negated=False, namespace=None)
                                AttrExpr(value='co', attr_path=AttrPath, comp_value=CompValue)
                                    AttrPath(attr_name='value', sub_attr=None, uri=None)
                                    CompValue(value='@example.com')
                    AttrPath(attr_name='emails', sub_attr=None, uri=None)
            Filter(expr=Filter, negated=False, namespace=None)
                Filter(expr=Filter, negated=False, namespace=AttrPath)
                    Filter(expr=LogExpr, negated=False, namespace=None)
                        LogExpr(op='and', expr1=Filter, expr2=Filter)
                            Filter(expr=AttrExpr, negated=False, namespace=None)
                                AttrExpr(value='eq', attr_path=AttrPath, comp_value=CompValue)
                                    AttrPath(attr_name='type', sub_attr=None, uri=None)
                                    CompValue(value='xmpp')
                            Filter(expr=AttrExpr, negated=False, namespace=None)
                                AttrExpr(value='co', attr_path=AttrPath, comp_value=CompValue)
                                    AttrPath(attr_name='value', sub_attr=None, uri=None)
                                    CompValue(value='@foo.com')
                    AttrPath(attr_name='ims', sub_attr=None, uri=None)

The third step is to transpile this AST into a language of our choice.
The above query is transpiled to SQL below.

::

    sfp-transpiler 'emails[type eq "work" and value co "@example.com"] or ims[type eq "xmpp" and value co "@foo.com"]'

    ((emails.type = {0}) AND (emails.value LIKE {1})) OR ((ims.type = {2}) AND (ims.value LIKE {3}))
    {0: 'work', 1: '%@example.com%', 2: 'xmpp', 3: '%@foo.com%'}

The fourth step is to take what is a segment of a SQL WHERE clause and complete
the rest of the SQL query.

::

    sfp-query 'emails[type eq "work" and value co "@example.com"] or ims[type eq "xmpp" and value co "@foo.com"]'

    >>> DO NOT USE THIS OUTPUT DIRECTLY
    >>> SQL INJECTION ATTACK RISK
    >>> SQL PREVIEW:
        SELECT DISTINCT users.*
        FROM users
        LEFT JOIN emails ON emails.user_id = users.id
        LEFT JOIN schemas ON schemas.user_id = users.id
        WHERE ((emails.type = work) AND (emails.value LIKE %@example.com%)) OR ((ims.type = xmpp) AND (ims.value LIKE %@foo.com%));

Please note that SFP does not build SQL queries with parameters pre-injected.
That would create a SQL injection attack vulnerability. Instead a ``SQLQuery``
object is created and can be forced to display itself as seen above
by ``print`` ing the query object.

Installation
------------
::

    pip install scim2-filter-parser

    # Or ...

    pip install scim2-filter-parser[django-query]

Use
---

Although command line shims are provided, the library is intended to be used
programmatically. Users of the library should instantiate the
``scim2_filter_parser.queries.SQLQuery`` class with an attribute map and optionally
any joins necessary to make all required fields accessible in the query.

For example, if user information is stored in the ``users`` table and email
information is stored in a different table ``emails``, then the attribute map
and the joins might be defined as so::

    from scim2_filter_parser.queries import SQLQuery

    attr_map = {
        ('userName', None, None): 'users.username',
        ('name', 'familyName', None): 'users.family_name',
        ('meta', 'lastModified', None): 'users.update_ts',
        ('emails', None, None): 'emails.address',
        ('emails', 'value', None): 'emails.address',
    }

    joins = (
        'LEFT JOIN emails ON emails.user_id = users.id',
    )

    filter_ = 'name.familyName co "Simpson" or emails.value eq "lisa@example.com"'

    q = SQLQuery(filter_, 'users', attr_map, joins)

    q.sql # Will be...

    SELECT DISTINCT users.*
    FROM users
    LEFT JOIN emails ON emails.user_id = users.id
    WHERE (users.family_name LIKE %s) OR (emails.address = %s);

    q.params # Will be...

    ['%Simpson%', 'lisa@example.com']

The attribute_map (``attr_map``) is a mapping of SCIM attribute, subattribute,
and schema uri to a table field. You will need to customize this to your
particular database schema.

The ``SQLQuery.sql`` method returns SQL that can be used as the first
argument in a call to ``cursor.execute()`` with your favorite DB engine.
If you are using a database that requires a replacement character other than '%s',
then you can subclass the ``SQLQuery`` class and override the ``placeholder`` class
level variable. See the query module and unit tests for an example of this subclassing
with SQLite.

The ``SQLQuery.params`` method returns a list of items that can be used as the
second argument in a call to ``cursor.execute()``.

Django
------

If you would like to produce a `Django Q`_ object instead of a raw SQL query, you can pass
a SCIM filter query and attribute map to the ``get_query`` function from the module
``scim2_filter_parser.transpilers.django_q_object``. For example::

    get_query(scim_query: str, attr_map: Mapping)

This Q object can then be passed to a Django filter query like so::

    query = get_query(scim_query, attr_map)
    User.objects.filter(query)

Please note that you will need to install the Django Query extra like for this feature to be available::

    pip install scim2-filter-parser[django-query]

.. _`Django Q`: https://docs.djangoproject.com/en/3.1/topics/db/queries/#complex-lookups-with-q-objects

Speed
-----

SFP is pretty fast. Check out the speed_test.py script for details on the long and short
filter queries tested. SFP transpiled a short filter query into SQL in under 54 microseconds.
For a longer query, SFP only took 273 microseconds.

::

    ➜  scim2-filter-parser git:(master) ✗ python -m timeit -s "import speed_test" "speed_test.short()"
    10000 loops, best of 3: 53.8 usec per loop
    ➜  scim2-filter-parser git:(master) ✗ python -m timeit -s "import speed_test" "speed_test.long()"
    1000 loops, best of 3: 273 usec per loop

Development Speed
-----------------

Since this project is relatively stable, time is only dedicated to it on
Fridays. Thus if you issue a PR, bug, etc, please note that it may take a week
before we get back to you. Thanks you for your patience.

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

This project uses Poetry to manage dependencies, etc. Thus to install the
necessary tools when developing, run:

::

    poetry install -v --extras "django-query"

Tests
-----

.. |tests| image:: https://github.com/15five/scim2-filter-parser/workflows/CI%2FCD/badge.svg
    :target: https://github.com/15five/scim2-filter-parser/actions

https://github.com/15five/scim2-filter-parser/actions

Tests are typically run locally with `tox` (https://tox.wiki/). Tox will test
all supported versions of Python.

```
tox
```

To run the test suite with a single version of Python (the version you created
the virtualenv with), run:

::

    poetry run pytest tests/

Coverage
--------

.. |coverage| image:: https://codecov.io/gh/15five/scim2-filter-parser/graph/badge.svg
    :target: https://codecov.io/gh/15five/scim2-filter-parser

https://codecov.io/gh/15five/scim2-filter-parser

::

    tox -e coverage


Deployment
----------

https://pypi.org/project/scim2-filter-parser/

To deploy this package to PyPI, run:

::

    poetry build
    poetry publish

            

Raw data

            {
    "_id": null,
    "home_page": "https://pypi.org/project/scim2-filter-parser/",
    "name": "scim2-filter-parser",
    "maintainer": "Paul Logston",
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": "paul.logston@gmail.com",
    "keywords": "scim, scim2, 2.0, filter",
    "author": "Paul Logston",
    "author_email": "paul@15five.com",
    "download_url": "https://files.pythonhosted.org/packages/25/c0/2f4d5caee8faa8f0d0979584c4aab95fa69d626d6f9d211dd6bb7089bc2f/scim2_filter_parser-0.7.0.tar.gz",
    "platform": null,
    "description": "SCIM 2.0 Filter Parser\n======================\n\n|github| |codecov| |docs|\n\n.. |codecov| image:: https://codecov.io/gh/15five/scim2-filter-parser/branch/master/graph/badge.svg\n  :target: https://codecov.io/gh/15five/scim2-filter-parser\n\n.. |docs| image:: https://readthedocs.org/projects/scim2-filter-parser/badge/?version=latest\n  :target: https://scim2-filter-parser.readthedocs.io/en/latest/?badge=latest\n  :alt: Documentation Status\n\n.. |github| image:: https://github.com/15five/scim2-filter-parser/workflows/CI%2FCD/badge.svg\n  :target: https://github.com/15five/scim2-filter-parser/actions?workflow=CI%2FCD\n  :alt: CI/CD Status\n\nDescription\n-----------\n\nSCIM 2.0 defines queries that look like this::\n\n    'emails[type eq \"work\" and value co \"@example.com\"] or ims[type eq \"xmpp\" and value co \"@foo.com\"]'\n\nThese can be hard to work with and covert into SQL to run against a database.\n\nThat's where SCIM 2.0 Filter Parser (SFP) can help.\n\nSFP is broken up into four modules, each handling a different part of\ntranslating a SCIM call into a SQL query.\n\nThe first step is tokenization or lexical analysis where the filter query\nis broken down into many tokens that make it up.\n\n::\n\n    sfp-lexer 'emails[type eq \"work\" and value co \"@example.com\"] or ims[type eq \"xmpp\" and value co \"@foo.com\"]'\n\n    Token(type='ATTRNAME', value='emails', lineno=1, index=0)\n    Token(type='LBRACKET', value='[', lineno=1, index=6)\n    Token(type='ATTRNAME', value='type', lineno=1, index=7)\n    Token(type='EQ', value='eq', lineno=1, index=12)\n    Token(type='COMP_VALUE', value='work', lineno=1, index=15)\n    Token(type='AND', value='and', lineno=1, index=22)\n    Token(type='ATTRNAME', value='value', lineno=1, index=26)\n    Token(type='CO', value='co', lineno=1, index=32)\n    Token(type='COMP_VALUE', value='@example.com', lineno=1, index=35)\n    Token(type='RBRACKET', value=']', lineno=1, index=49)\n    Token(type='OR', value='or', lineno=1, index=51)\n    Token(type='ATTRNAME', value='ims', lineno=1, index=54)\n    Token(type='LBRACKET', value='[', lineno=1, index=57)\n    Token(type='ATTRNAME', value='type', lineno=1, index=58)\n    Token(type='EQ', value='eq', lineno=1, index=63)\n    Token(type='COMP_VALUE', value='xmpp', lineno=1, index=66)\n    Token(type='AND', value='and', lineno=1, index=73)\n    Token(type='ATTRNAME', value='value', lineno=1, index=77)\n    Token(type='CO', value='co', lineno=1, index=83)\n    Token(type='COMP_VALUE', value='@foo.com', lineno=1, index=86)\n    Token(type='RBRACKET', value=']', lineno=1, index=96)\n\n\nThe second step is to convert that series of tokens into a abstract syntax tree.\n\n::\n\n    sfp-parser 'emails[type eq \"work\" and value co \"@example.com\"] or ims[type eq \"xmpp\" and value co \"@foo.com\"]'\n\n    Filter(expr=LogExpr, negated=False, namespace=None)\n        LogExpr(op='or', expr1=Filter, expr2=Filter)\n            Filter(expr=Filter, negated=False, namespace=None)\n                Filter(expr=Filter, negated=False, namespace=AttrPath)\n                    Filter(expr=LogExpr, negated=False, namespace=None)\n                        LogExpr(op='and', expr1=Filter, expr2=Filter)\n                            Filter(expr=AttrExpr, negated=False, namespace=None)\n                                AttrExpr(value='eq', attr_path=AttrPath, comp_value=CompValue)\n                                    AttrPath(attr_name='type', sub_attr=None, uri=None)\n                                    CompValue(value='work')\n                            Filter(expr=AttrExpr, negated=False, namespace=None)\n                                AttrExpr(value='co', attr_path=AttrPath, comp_value=CompValue)\n                                    AttrPath(attr_name='value', sub_attr=None, uri=None)\n                                    CompValue(value='@example.com')\n                    AttrPath(attr_name='emails', sub_attr=None, uri=None)\n            Filter(expr=Filter, negated=False, namespace=None)\n                Filter(expr=Filter, negated=False, namespace=AttrPath)\n                    Filter(expr=LogExpr, negated=False, namespace=None)\n                        LogExpr(op='and', expr1=Filter, expr2=Filter)\n                            Filter(expr=AttrExpr, negated=False, namespace=None)\n                                AttrExpr(value='eq', attr_path=AttrPath, comp_value=CompValue)\n                                    AttrPath(attr_name='type', sub_attr=None, uri=None)\n                                    CompValue(value='xmpp')\n                            Filter(expr=AttrExpr, negated=False, namespace=None)\n                                AttrExpr(value='co', attr_path=AttrPath, comp_value=CompValue)\n                                    AttrPath(attr_name='value', sub_attr=None, uri=None)\n                                    CompValue(value='@foo.com')\n                    AttrPath(attr_name='ims', sub_attr=None, uri=None)\n\nThe third step is to transpile this AST into a language of our choice.\nThe above query is transpiled to SQL below.\n\n::\n\n    sfp-transpiler 'emails[type eq \"work\" and value co \"@example.com\"] or ims[type eq \"xmpp\" and value co \"@foo.com\"]'\n\n    ((emails.type = {0}) AND (emails.value LIKE {1})) OR ((ims.type = {2}) AND (ims.value LIKE {3}))\n    {0: 'work', 1: '%@example.com%', 2: 'xmpp', 3: '%@foo.com%'}\n\nThe fourth step is to take what is a segment of a SQL WHERE clause and complete\nthe rest of the SQL query.\n\n::\n\n    sfp-query 'emails[type eq \"work\" and value co \"@example.com\"] or ims[type eq \"xmpp\" and value co \"@foo.com\"]'\n\n    >>> DO NOT USE THIS OUTPUT DIRECTLY\n    >>> SQL INJECTION ATTACK RISK\n    >>> SQL PREVIEW:\n        SELECT DISTINCT users.*\n        FROM users\n        LEFT JOIN emails ON emails.user_id = users.id\n        LEFT JOIN schemas ON schemas.user_id = users.id\n        WHERE ((emails.type = work) AND (emails.value LIKE %@example.com%)) OR ((ims.type = xmpp) AND (ims.value LIKE %@foo.com%));\n\nPlease note that SFP does not build SQL queries with parameters pre-injected.\nThat would create a SQL injection attack vulnerability. Instead a ``SQLQuery``\nobject is created and can be forced to display itself as seen above\nby ``print`` ing the query object.\n\nInstallation\n------------\n::\n\n    pip install scim2-filter-parser\n\n    # Or ...\n\n    pip install scim2-filter-parser[django-query]\n\nUse\n---\n\nAlthough command line shims are provided, the library is intended to be used\nprogrammatically. Users of the library should instantiate the\n``scim2_filter_parser.queries.SQLQuery`` class with an attribute map and optionally\nany joins necessary to make all required fields accessible in the query.\n\nFor example, if user information is stored in the ``users`` table and email\ninformation is stored in a different table ``emails``, then the attribute map\nand the joins might be defined as so::\n\n    from scim2_filter_parser.queries import SQLQuery\n\n    attr_map = {\n        ('userName', None, None): 'users.username',\n        ('name', 'familyName', None): 'users.family_name',\n        ('meta', 'lastModified', None): 'users.update_ts',\n        ('emails', None, None): 'emails.address',\n        ('emails', 'value', None): 'emails.address',\n    }\n\n    joins = (\n        'LEFT JOIN emails ON emails.user_id = users.id',\n    )\n\n    filter_ = 'name.familyName co \"Simpson\" or emails.value eq \"lisa@example.com\"'\n\n    q = SQLQuery(filter_, 'users', attr_map, joins)\n\n    q.sql # Will be...\n\n    SELECT DISTINCT users.*\n    FROM users\n    LEFT JOIN emails ON emails.user_id = users.id\n    WHERE (users.family_name LIKE %s) OR (emails.address = %s);\n\n    q.params # Will be...\n\n    ['%Simpson%', 'lisa@example.com']\n\nThe attribute_map (``attr_map``) is a mapping of SCIM attribute, subattribute,\nand schema uri to a table field. You will need to customize this to your\nparticular database schema.\n\nThe ``SQLQuery.sql`` method returns SQL that can be used as the first\nargument in a call to ``cursor.execute()`` with your favorite DB engine.\nIf you are using a database that requires a replacement character other than '%s',\nthen you can subclass the ``SQLQuery`` class and override the ``placeholder`` class\nlevel variable. See the query module and unit tests for an example of this subclassing\nwith SQLite.\n\nThe ``SQLQuery.params`` method returns a list of items that can be used as the\nsecond argument in a call to ``cursor.execute()``.\n\nDjango\n------\n\nIf you would like to produce a `Django Q`_ object instead of a raw SQL query, you can pass\na SCIM filter query and attribute map to the ``get_query`` function from the module\n``scim2_filter_parser.transpilers.django_q_object``. For example::\n\n    get_query(scim_query: str, attr_map: Mapping)\n\nThis Q object can then be passed to a Django filter query like so::\n\n    query = get_query(scim_query, attr_map)\n    User.objects.filter(query)\n\nPlease note that you will need to install the Django Query extra like for this feature to be available::\n\n    pip install scim2-filter-parser[django-query]\n\n.. _`Django Q`: https://docs.djangoproject.com/en/3.1/topics/db/queries/#complex-lookups-with-q-objects\n\nSpeed\n-----\n\nSFP is pretty fast. Check out the speed_test.py script for details on the long and short\nfilter queries tested. SFP transpiled a short filter query into SQL in under 54 microseconds.\nFor a longer query, SFP only took 273 microseconds.\n\n::\n\n    \u279c  scim2-filter-parser git:(master) \u2717 python -m timeit -s \"import speed_test\" \"speed_test.short()\"\n    10000 loops, best of 3: 53.8 usec per loop\n    \u279c  scim2-filter-parser git:(master) \u2717 python -m timeit -s \"import speed_test\" \"speed_test.long()\"\n    1000 loops, best of 3: 273 usec per loop\n\nDevelopment Speed\n-----------------\n\nSince this project is relatively stable, time is only dedicated to it on\nFridays. Thus if you issue a PR, bug, etc, please note that it may take a week\nbefore we get back to you. Thanks you for your patience.\n\nDevelopment\n-----------\n\nThis project uses Poetry to manage dependencies, etc. Thus to install the\nnecessary tools when developing, run:\n\n::\n\n    poetry install -v --extras \"django-query\"\n\nTests\n-----\n\n.. |tests| image:: https://github.com/15five/scim2-filter-parser/workflows/CI%2FCD/badge.svg\n    :target: https://github.com/15five/scim2-filter-parser/actions\n\nhttps://github.com/15five/scim2-filter-parser/actions\n\nTests are typically run locally with `tox` (https://tox.wiki/). Tox will test\nall supported versions of Python.\n\n```\ntox\n```\n\nTo run the test suite with a single version of Python (the version you created\nthe virtualenv with), run:\n\n::\n\n    poetry run pytest tests/\n\nCoverage\n--------\n\n.. |coverage| image:: https://codecov.io/gh/15five/scim2-filter-parser/graph/badge.svg\n    :target: https://codecov.io/gh/15five/scim2-filter-parser\n\nhttps://codecov.io/gh/15five/scim2-filter-parser\n\n::\n\n    tox -e coverage\n\n\nDeployment\n----------\n\nhttps://pypi.org/project/scim2-filter-parser/\n\nTo deploy this package to PyPI, run:\n\n::\n\n    poetry build\n    poetry publish\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "A customizable parser/transpiler for SCIM2.0 filters.",
    "version": "0.7.0",
    "project_urls": {
        "Documentation": "https://scim2-filter-parser.readthedocs.io/en/stable/",
        "Homepage": "https://pypi.org/project/scim2-filter-parser/",
        "Repository": "https://github.com/15five/scim2-filter-parser"
    },
    "split_keywords": [
        "scim",
        " scim2",
        " 2.0",
        " filter"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "5754b54961bfc5018fa593758c439fe0d4a22fbadfabff49a7559850af9a79e1",
                "md5": "989861d6564cd3b487e393d10044ba89",
                "sha256": "a74f90a2d52a77e0f1bc4d77e84b79f88749469f6f7192d64a4f92e4fe50ab69"
            },
            "downloads": -1,
            "filename": "scim2_filter_parser-0.7.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "989861d6564cd3b487e393d10044ba89",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 23409,
            "upload_time": "2024-07-20T16:38:21",
            "upload_time_iso_8601": "2024-07-20T16:38:21.525943Z",
            "url": "https://files.pythonhosted.org/packages/57/54/b54961bfc5018fa593758c439fe0d4a22fbadfabff49a7559850af9a79e1/scim2_filter_parser-0.7.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "25c02f4d5caee8faa8f0d0979584c4aab95fa69d626d6f9d211dd6bb7089bc2f",
                "md5": "a0bf9826a998364526e2e3743edb903b",
                "sha256": "1e11dbe2e186fc1be6d93732b467a3bbaa9deff272dfeb3a0540394cfab7030c"
            },
            "downloads": -1,
            "filename": "scim2_filter_parser-0.7.0.tar.gz",
            "has_sig": false,
            "md5_digest": "a0bf9826a998364526e2e3743edb903b",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 21358,
            "upload_time": "2024-07-20T16:38:23",
            "upload_time_iso_8601": "2024-07-20T16:38:23.200393Z",
            "url": "https://files.pythonhosted.org/packages/25/c0/2f4d5caee8faa8f0d0979584c4aab95fa69d626d6f9d211dd6bb7089bc2f/scim2_filter_parser-0.7.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-07-20 16:38:23",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "15five",
    "github_project": "scim2-filter-parser",
    "travis_ci": false,
    "coveralls": true,
    "github_actions": true,
    "tox": true,
    "lcname": "scim2-filter-parser"
}
        
Elapsed time: 9.48733s