{"id":40953155,"url":"https://github.com/pjwerneck/pyrql","last_synced_at":"2026-01-22T05:16:53.993Z","repository":{"id":44707568,"uuid":"71020690","full_name":"pjwerneck/pyrql","owner":"pjwerneck","description":"Python RQL Parser","archived":false,"fork":false,"pushed_at":"2025-11-08T17:06:59.000Z","size":613,"stargazers_count":16,"open_issues_count":0,"forks_count":5,"subscribers_count":2,"default_branch":"develop","last_synced_at":"2025-11-27T19:46:51.859Z","etag":null,"topics":["parser","query-language","querystring-parser","rql","rql-syntax"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pjwerneck.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2016-10-16T00:03:50.000Z","updated_at":"2025-11-08T17:06:31.000Z","dependencies_parsed_at":"2025-03-15T17:37:28.672Z","dependency_job_id":null,"html_url":"https://github.com/pjwerneck/pyrql","commit_stats":{"total_commits":112,"total_committers":4,"mean_commits":28.0,"dds":0.0357142857142857,"last_synced_commit":"b4e7c68ae6ac39e853b6bce7bbc686c9ee2835a9"},"previous_names":[],"tags_count":41,"template":false,"template_full_name":null,"purl":"pkg:github/pjwerneck/pyrql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjwerneck%2Fpyrql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjwerneck%2Fpyrql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjwerneck%2Fpyrql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjwerneck%2Fpyrql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pjwerneck","download_url":"https://codeload.github.com/pjwerneck/pyrql/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjwerneck%2Fpyrql/sbom","scorecard":{"id":736306,"data":{"date":"2025-08-11","repo":{"name":"github.com/pjwerneck/pyrql","commit":"599903e306f550e956706bde67baef5bb5d0cd75"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.6,"checks":[{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":2,"reason":"3 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 2","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/pytest.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pytest.yml:19: update your workflow using https://app.stepsecurity.io/secureworkflow/pjwerneck/pyrql/pytest.yml/develop?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pytest.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/pjwerneck/pyrql/pytest.yml/develop?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pytest.yml:29: update your workflow using https://app.stepsecurity.io/secureworkflow/pjwerneck/pyrql/pytest.yml/develop?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/pytest.yml:36: update your workflow using https://app.stepsecurity.io/secureworkflow/pjwerneck/pyrql/pytest.yml/develop?enable=pin","Info:   0 out of   3 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'develop'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-22T15:58:07.384Z","repository_id":44707568,"created_at":"2025-08-22T15:58:07.384Z","updated_at":"2025-08-22T15:58:07.384Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28655543,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-22T01:17:37.254Z","status":"online","status_checked_at":"2026-01-22T02:00:07.137Z","response_time":144,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["parser","query-language","querystring-parser","rql","rql-syntax"],"created_at":"2026-01-22T05:16:53.927Z","updated_at":"2026-01-22T05:16:53.980Z","avatar_url":"https://github.com/pjwerneck.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pyrql\n\n[![Build Status](https://github.com/pjwerneck/pyrql/actions/workflows/pytest.yml/badge.svg?branch=develop)](https://github.com/pjwerneck/pyrql/actions/workflows/pytest.yml)\n[![PyPI version](https://badge.fury.io/py/pyrql.svg)](https://badge.fury.io/py/pyrql)\n[![Python Versions](https://img.shields.io/pypi/pyversions/pyrql.svg)](https://pypi.org/project/pyrql/)\n[![PyPI Downloads](https://static.pepy.tech/badge/pyrql)](https://pepy.tech/projects/pyrql)\n\n## Table of Contents\n- [pyrql](#pyrql)\n  - [Table of Contents](#table-of-contents)\n  - [Overview](#overview)\n  - [Stability](#stability)\n  - [Installation](#installation)\n  - [Documentation](#documentation)\n    - [RQL Syntax](#rql-syntax)\n      - [Typed Values](#typed-values)\n      - [URL encoding](#url-encoding)\n      - [Limitations](#limitations)\n    - [Query Engine](#query-engine)\n      - [Example](#example)\n    - [Reference Table](#reference-table)\n\n## Overview\n\nResource Query Language (RQL) is a query language designed for use in URIs, with object-style data structures.\n\nThis library provides:\n- A Python parser that produces output identical to the [JavaScript Library](https://github.com/persvr/rql)\n- A query engine that can perform RQL queries on lists of dictionaries\n\n## Stability\n\nThis library follows semantic versioning. Version 0.8.0 marks the stable public API, which includes the `parse()`, `unparse()`, `Query` class, and exception classes. Future releases will maintain backward compatibility within major versions.\n\n## Installation\n\n```bash\npip install pyrql\n```\n\n\n## Documentation\n\n### RQL Syntax\n\nThe RQL syntax is a compatible superset of the standard HTML form URL encoding. Simple queries can be written in standard HTML form URL encoding, but more complex queries can be written in a URL friendly query string, using a set of nested operators.\n\nFor example, querying for a property `foo` with the value of `3` could be written as:\n\n```\neq(foo,3)\n```\n\nOr in standard HTML form URL encoding:\n\n```\nfoo=3\n```\n\nBoth expressions result in the exact same parsed value:\n\n```\n{'name': 'eq', 'args': ['foo', 3]}\n```\n\n\n#### Typed Values\n\nThe following types are available:\n\n- string\n- number\n- boolean\n- null\n- epoch\n- date\n- datetime\n- uuid\n- decimal\n\nNumbers, booleans and null are converted automatically to the corresponding Python types. Numbers are converted to float or integer accordingly:\n\n```\n\u003e\u003e\u003e pyrql.parse('ten=10')\n{'name': 'eq', 'args': ['ten', 10]}\n\u003e\u003e\u003e pyrql.parse('pi=3.14')\n{'name': 'eq', 'args': ['pi', 3.14]}\n\u003e\u003e\u003e pyrql.parse('mil=1e6')\n{'name': 'eq', 'args': ['mil', 1000000.0]}\n```\n\nBooleans and null are converted to booleans and None:\n\n```\n\u003e\u003e\u003e pyrql.parse('a=true')\n{'name': 'eq', 'args': ['a', True]}\n\u003e\u003e\u003e pyrql.parse('a=false')\n{'name': 'eq', 'args': ['a', False]}\n\u003e\u003e\u003e pyrql.parse('a=null')\n{'name': 'eq', 'args': ['a', None]}\n```\n\nTypes can be used explicitly in the form `type:value`:\n\n```\n\u003e\u003e\u003e pyrql.parse('a=string:1')\n{'name': 'eq', 'args': ['a', '1']}\n\n```\n\n#### URL encoding\n\nThe parser automatically unquotes strings with percent-encoding, but it also accepts characters that would require encoding if submitted on an URI.\n\n```\n\u003e\u003e\u003e pyrql.parse('eq(foo,lero lero)')\n{'name': 'eq', 'args': ['foo', 'lero lero']}\n\u003e\u003e\u003e pyrql.parse('eq(foo,lero%20lero)')\n{'name': 'eq', 'args': ['foo', 'lero lero']}\n```\n\nIf that's undesirable, you should verify the URL before calling the parser.\n\n\n#### Limitations\n\nThe pyrql parser doesn't implement a few redundant details of the RQL syntax, either because the standard isn't clear on what's allowed, or the functionality is already available in a clearer syntax.\n\nThe only operator allowed at the query top level is the AND operator, i.e. `\u0026`. A toplevel `or` operation using the `|` operator must be enclosed in parenthesis.\n\n```\n\u003e\u003e\u003e pyrql.parse('(a=1|b=2)')\n{'args': [{'args': ['a', 1], 'name': 'eq'}, {'args': ['b', 2], 'name': 'eq'}], 'name': 'or'}\n```\n\nThe slash syntax for arrays is not implemented yet and will result in a syntax error. The only valid array syntax is the comma delimited list inside parenthesis:\n\n```\n\u003e\u003e\u003e pyrql.parse('(a,b)=1')\n{'args': [('a', 'b'), 1], 'name': 'eq'}\n```\n\n### Query Engine\n\nThe main use case for the query engine is to allow API clients to perform server-side filtering on large responses on their own. It's an easy drop-in improvement when you want to provide simple querying capabilities on an existing API endpoint without exposing your storage, or reimplementing everything in a more complete querying solution like GraphQL.\n\nThe data is fed through the operators in the query from left to right, as a pipeline, where the results of each top-level operator are fed to the next. If you're familiar with MongoDB aggregation pipelines, the query engine follows a similar concept, where each step transforms the current state of the data before being fed to the next step.\n\nThe operators can be categorized in three types:\n\n- Filtering operators, which filter the data, like comparison and membership operators.\n- Transforming operators, which transform all the data at once, like `select`, `sort` and `aggregate`.\n- Aggregation operators, which reduce all data to a single value, like `sum` and `min`.\n\nSee the reference below for all operators and the equivalent Python code.\n\n#### Example\n\nFor example, if you have a Flask API with an endpoint exposing tasks, like this:\n\n```python\n@app.route('/api/v1/tasks')\ndef get_user_tasks():\n    tasks = [task.to_dict() for task in Task.get_all()]\n    return jsonify(tasks)\n```\nAdding pyrql query support is straightforward:\n\n```python\nfrom pyrql import Query\nfrom urllib.parse import unquote\n\n@app.route('/api/v1/tasks')\ndef get_user_tasks():\n    tasks = [task.to_dict() for task in Task.get_all()]\n\n    query_string = unquote(request.query_string.decode(request.charset))\n    query = Query(tasks).query(query_string)\n\n    return jsonify(query.all())\n```\n\nAnd now the endpoint supports the RQL syntax. For sake of example, let's consider a typical tasks response is similar to the following:\n\n```json\n[\n    {\n    \"status\": \"PENDING\",\n    \"name\": \"Update mobile app\",\n    \"due_date\": \"2022-02-01T15:00:00\",\n    \"completed_date\": null,\n    \"tags\": [\"development\", \"easy\"],\n    \"assigned_to\": null,\n    \"hours_budgeted\": 4,\n    \"hours_spent\": 0\n    },\n    {\n    \"status\": \"COMPLETED\",\n    \"name\": \"Design new frontend\",\n    \"due_date\": \"2022-01-28T14:00:00\",\n    \"completed_date\": \"2022-01-27T12:17:00\"\n    \"tags\": [\"design\", \"medium\"],\n    \"assigned_to\": \"Bill\",\n    \"hours_budgeted\": 8,\n    \"hours_spent\": 6\n    },\n    ...\n]\n```\n\nIf an API client wants to retrieve only tasks in the `PENDING` status, the simple equality comparison is supported with standard query strings:\n\n```http\nGET /api/v1/asks?state=PENDING\n```\nOr with the RQL syntax:\n\n```http\nGET /api/v1/tasks?eq(state,PENDING)\n```\n\nLet's say the client wants tasks in the `PENDING` state which contain the `easy` tag:\n\n```http\nGET /api/v1/tasks?eq(state,PENDING)\u0026contains(tags,easy)\n```\n\nIt can also perform simple aggregations, like adding up all hours spent by completed tasks, for each assigned user:\n\n```http\nGET /api/v1/tasks?eq(state,COMPLETED)\u0026ne(assigned_user,null)\u0026aggregate(assigned_to,sum(hours_spent))\n```\n\n### Reference Table\n\n\n| RQL                                  | Python equivalent                                           | Obs.                                   |\n| ------------------------------------ |:----------------------------------------------------------- |:-------------------------------------- |\n| FILTERING                            |                                                             |                                        |\n| `eq(key,value)`                      | `[row for row in data if row[key] == value] `               |                                        |\n| `ne(key,value)`                      | `[row for row in data if row[key] != value]`                |                                        |\n| `lt(key,value)`                      | `[row for row in data if row[key] \u003c value]`                 |                                        |\n| `le(key,value)`                      | `[row for row in data if row[key] \u003c= value]`                |                                        |\n| `gt(key,value)`                      | `[row for row in data if row[key] \u003e value]`                 |                                        |\n| `ge(key,value)`                      | `[row for row in data if row[key] \u003e= value]`                |                                        |\n| `in(key,value)`                      | `[row for row in data if row[key] in value]`                |                                        |\n| `out(key,value)`                     | `[row for row in data if row[key] not in value]`            |                                        |\n| `contains(key,value)`                | `[row for row in data if value in row[key]]`                |                                        |\n| `excludes(key,value)`                | `[row for row in data if value not in row[key]]`            |                                        |\n| `and(expr1,expr2,...)`               | `[row for row in data if expr1 and expr2]`                  |                                        |\n| `or(expr1,expr2,...)`                | `[row for row in data if expr1 or expr2]`                   |                                        |\n| TRANSFORMING                         |                                                             | All operators return lists.            |\n| `select(a,b,c,...)`                  | `[{a: row[a], b: row[b], c: row[c]} for row in data]`       |                                        |\n| `values(a)`                          | `[row[a] for row in data]`                                  |                                        |\n| `limit(count,start?)`                | `data[start:start+count]`                                   |                                        |\n| `sort(key)`                          | `sorted(data, key=lambda row: row[key])`                    |                                        |\n| `sort(-key)`                         | `sorted(data, key=lambda row: row[key], reverse=True)`      |                                        |\n| `distinct()`                         | `list(dict.fromkeys(data))`                                 | Preserves order.                       |\n| `first()`                            | `data[:1]`                                                  |                                        |\n| `one()`                              | `data`                                                      | Raises RQLQueryError if len(data) != 1 |\n| `aggregate(key,agg1(a),agg2(b),...)` | See below                                                   |                                        |\n| `unwind(key)`                        | `[{**row, key: item} for row in data for item in row[key]]` |                                        |\n| AGGREGATION                          |                                                             |                                        |\n| `sum(key)`                           | `sum([row[key] for row in data])`                           |                                        |\n| `mean(key)`                          | `statistics.mean([row[key] for row in data])`               |                                        |\n| `max(key)`                           | `max([row[key] for row in data])`                           |                                        |\n| `min(key)`                           | `min([row[key] for row in data])`                           |                                        |\n| `count()`                            | `len(data)`                                                 |                                        |\n\n\nThe `aggregate` operator can't be summarized in a readable one-liner. It accepts a key, and any number of aggregation operators. All the data is grouped by the key value, aggregated by each aggregation operator, and a new list is built with the results and key value.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpjwerneck%2Fpyrql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpjwerneck%2Fpyrql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpjwerneck%2Fpyrql/lists"}