qlsq


Nameqlsq JSON
Version 0.5.0 PyPI version JSON
download
home_pageNone
Summarydynamic sql generetor
upload_time2025-07-17 19:23:10
maintainerNone
docs_urlNone
authorNone
requires_python>=3.12
licenseNone
keywords sql postgresql sql-generator
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # qlsq (QL²) - Predictable SQL Query Generation

[![PyPI version](https://badge.fury.io/py/qlsq.svg)](https://pypi.org/project/qlsq/)

**qlsq** (QL-squared) is a Python library for generating predictable, secure SQL queries from Lisp-like query expressions. It helps you build complex database queries while maintaining control over performance and security.

## ✨ Key Features

- **Prevents N+1 Query Problems** - Results nesing is not allowed
- **Query Complexity Control** - Restrict filtering to indexed columns only
- **Smart Join Management** - Only performs LEFT JOINs for actually selected fields
- **Claim-Based Access Control** - Fine-grained permissions for each field (read/edit/filter)
- **Predictable Output** - Generates clean, parameterized SQL queries
- **PostgreSQL Integration** - Works seamlessly with psycopg, generating parameterized queries

## šŸš€ Installation

```bash
pip install qlsq
# or
uv add qlsq
# or
poetry add qlsq
```

## šŸ“‹ Requirements

- Python 3.12+
- psycopg2 or psycopg3 (for PostgreSQL integration)

## šŸŽÆ Use Cases

Perfect for applications that need:
- Dynamic query building from frontend filters
- Multi-tenant applications with complex permissions
- APIs that expose flexible data querying capabilities
- Applications requiring predictable query performance

## šŸ“– Quick Start

### 1. Define Your Context

Every query operates within a context that defines tables and fields:

```python
from qlsq import ContextTable, ContextField, Context, QueryType

# Define tables and their relationships
tables = [
    ContextTable(
        alias="ut",
        source="user_tasks", 
        join_condition=None,  # Root table
        depends_on=[]
    ),
    ContextTable(
        alias="u",
        source="users",
        join_condition="u.id = ut.user_id",
        depends_on=["ut"]  # Depends on user_tasks table
    ),
]

# Define available fields with permissions
fields = [
    ContextField(
        alias="full_name",
        source="full_name",
        query_type=QueryType.text,
        table_alias="u",
        read_claim="r_full_name",
        edit_claim="e_full_name", 
        filter_claim="f_full_name",
    ),
    ContextField(
        alias="user_id",
        source="user_id",
        query_type=QueryType.numeric,
        table_alias="ut",
        read_claim="r_user_id",
        edit_claim="e_user_id",
        filter_claim="f_user_id",
    ),
]

# Create the context
context = Context(tables, fields)

# Create the context registry
context_registry = ContextRegistry(
    {
        "user_tasks": context,
    }
)
```

### 2. Write Lisp-Like Queries

```python
# Simple query: SELECT full_name WHERE user_id = 3
query_expression = [
    ["using", "user_tasks"], # specify context
    ["select", "full_name"],
    ["where", ["eq", "user_id", 3]]
]
```

### 3. Generate SQL (Two Approaches)

**Approach A: Parse then generate**
```python
# Parse and generate SQL
query = context.parse_query(query_expression)
sql, params = query.to_sql()

print("Generated SQL:")
print(sql)
# Output: SELECT u.full_name FROM user_tasks ut LEFT JOIN users u ON u.id = ut.user_id WHERE (ut.user_id = %(param_0)s);

print("Parameters:")
print(params)
# Output: {"param_0": 3}
```

**Approach B: Direct generation with claims**
```python
# Generate SQL directly with claims validation
user_claims = ["r_full_name", "f_user_id"]  # User's permissions
sql, params = context.to_sql(query_expression, user_claims)
```

### 4. Claims-Based Security

```python
# Define user permissions
user_claims = ["r_full_name", "f_user_id"]  # Can read full_name, filter by user_id

# This will work - user has required claims
query = context.parse_query([["select", "full_name"], ["where", ["eq", "user_id", 3]]])
query.assert_claims(user_claims)  # Validates permissions

# This will fail - user lacks r_user_id claim
try:
    query = context.parse_query([["select", "user_id"]])
    query.assert_claims(user_claims)  # Raises MissingClaimsError
except MissingClaimsError as e:
    print(f"Access denied: {e}")
```

### 5. Execute with psycopg

```python
import psycopg2

# Execute the query
with psycopg2.connect(database_url) as conn:
    with conn.cursor() as cursor:
        cursor.execute(sql, params)
        results = cursor.fetchall()
```

## šŸ” Advanced Examples

### Complex Filtering

```python
# Multiple conditions with AND/OR logic
query = [
    ["using", "user_tasks"],
    ["select", "full_name", "user_id"],
    ["where", [
        "and",
        ["eq", "user_id", 3],
        ["like", "full_name", ["str", "%john%"]]
    ]]
]
```

### Mathematical Operations

```python
# Arithmetic operations
query = [
    ["select", ["add", "field1", "field2"]],  # Addition
    ["select", ["sub", "field1", "field2"]],  # Subtraction  
    ["select", ["mul", "field1", "field2"]],  # Multiplication
    ["select", ["div", "field1", "field2"]],  # Division
]
```

### String Operations

```python
# String manipulation
query = [
    ["select", ["concat", "first_name", ["str", " "], "last_name"]],  # Concatenation
    ["select", ["lower", "full_name"]],   # Lowercase
    ["select", ["upper", "full_name"]],   # Uppercase
]
```

### Date Handling

```python
# Date operations
query = [
    ["where", ["eq", "created_at", ["date", "2024-01-15T10:30:00Z"]]]
]
```

### Null Checks and Coalescing

```python
# Working with NULL values
query = [
    ["select", ["coalesce", "nickname", ["str", "-NA-"]]],
    ["where", ["is_not_null", "email"]],  # Check for non-null
]
```

### Sorting and Limiting

```python
# Add sorting and pagination
query = [
    ["using", "users"],
    ["select", "full_name", "user_id"],
    ["where", ["gt", "user_id", 0]],
    ["orderby", ["asc", "full_name"]],  # Note: asc/desc wraps the field
    ["limit", 10],
    ["offset", 20]
]
```

### IN Clause and Complex Conditions

```python
# Multiple values and complex logic
query = [
    ["using", "users"],
    ["select", "full_name"],
    ["where", [
        "or",
        ["in", "user_id", 1, 2, 3, 4],
        ["and", 
            ["gte", "age", 18],
            ["like", "email", ["str", "%@company.com"]]
        ]
    ]]
]
```

## šŸ›”ļø Security Features

### Claim-Based Access Control

```python
# Only users with proper claims can access fields
user_claims = ["r_full_name", "f_user_id"]  # Can read full_name, filter by user_id
sql, params = context.to_sql(query_expression, user_claims) # Will raise MissingClaimsError if claims are missing
```

### Query Validation

- Prevents filtering on non-indexed columns (if configured)
- Validates field access permissions
- Ensures proper table relationships
- Protects against SQL injection through parameterization

## šŸŽØ Query Language Reference

### Core Operations
| Operation | Syntax | Example |
|-----------|--------|---------|
| Select | `["select", "field1", "field2"]` | `["select", "name", "email"]` |
| Where | `["where", condition]` | `["where", ["eq", "id", 1]]` |

### Comparison Operators
| Operation | Syntax | Example |
|-----------|--------|---------|
| Equals | `["eq", field, value]` | `["eq", "status", ["str", "active"]]` |
| Not Equals | `["neq", field, value]` | `["neq", "status", ["str", "deleted"]]` |
| Greater Than | `["gt", field, value]` | `["gt", "age", 18]` |
| Greater/Equal | `["gte", field, value]` | `["gte", "score", 100]` |
| Less Than | `["lt", field, value]` | `["lt", "price", 50]` |
| Less/Equal | `["lte", field, value]` | `["lte", "quantity", 10]` |
| Like Pattern | `["like", field, pattern]` | `["like", "name", ["str", "%john%"]]` |
| In List | `["in", field, val1, val2, ...]` | `["in", "id", 1, 2, 3]` |

### Logical Operators
| Operation | Syntax | Example |
|-----------|--------|---------|
| And | `["and", cond1, cond2, ...]` | `["and", ["eq", "a", 1], ["eq", "b", 2]]` |
| Or | `["or", cond1, cond2, ...]` | `["or", ["eq", "status", "active"], ["eq", "status", "pending"]]` |
| Not | `["not", condition]` | `["not", ["eq", "deleted", true]]` |

### Null Checks
| Operation | Syntax | Example |
|-----------|--------|---------|
| Is Null | `["is_null", field]` | `["is_null", "deleted_at"]` |
| Is Not Null | `["is_not_null", field]` | `["is_not_null", "email"]` |

### Mathematical Operations
| Operation | Syntax | Example |
|-----------|--------|---------|
| Addition | `["add", expr1, expr2, ...]` | `["add", "base_price", "tax"]` |
| Subtraction | `["sub", expr1, expr2, ...]` | `["sub", "total", "discount"]` |
| Multiplication | `["mul", expr1, expr2, ...]` | `["mul", "price", "quantity"]` |
| Division | `["div", expr1, expr2]` | `["div", "total", "count"]` |

### String Operations
| Operation | Syntax | Example |
|-----------|--------|---------|
| Concatenate | `["concat", str1, str2, ...]` | `["concat", "first_name", ["str", " "], "last_name"]` |
| Lowercase | `["lower", string_expr]` | `["lower", "email"]` |
| Uppercase | `["upper", string_expr]` | `["upper", "code"]` |
| Coalesce | `["coalesce", expr1, expr2, ...]` | `["coalesce", "nickname", ["str", "-NA-"]]` |

### Literal Values
| Type | Syntax | Example |
|------|--------|---------|
| String | `["str", "value"]` | `["str", "hello world"]` |
| Date | `["date", "iso_string"]` | `["date", "2024-01-15T10:30:00Z"]` |
| Integer | `42` | `["eq", "age", 25]` |
| Float | `3.14` | `["eq", "price", 19.99]` |
| Boolean | `true`/`false` | `["eq", "active", true]` |
| Null | `null` | `["eq", "deleted_at", null]` |

### Ordering and Pagination
| Operation | Syntax | Example |
|-----------|--------|---------|
| Order By | `["orderby", direction1, direction2, ...]` | `["orderby", ["asc", "name"], ["desc", "created_at"]]` |
| Ascending | `["asc", field]` | `["asc", "name"]` |
| Descending | `["desc", field]` | `["desc", "created_at"]` |
| Limit | `["limit", count]` | `["limit", 10]` |
| Offset | `["offset", count]` | `["offset", 20]` |

## āš ļø Limitations

- **No nested queries** - Complex nesting must be implemented in SQL
- **PostgreSQL only** - Currently only supports PostgreSQL via psycopg
- **Left joins only** - Developer must ensure there are no unwanted duplicates

## šŸ”§ API Reference

`FIXME`: list main classes, methods, exceptions


## šŸ¤ Contributing

Contributions are welcome! `main` branch is for development, each release and subseaquent hotfixes land on separate branches like `v0.1.3`.

## šŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

## šŸ”— Links

- [PyPI Package](https://pypi.org/project/qlsq/)

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "qlsq",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.12",
    "maintainer_email": null,
    "keywords": "sql, postgresql, sql-generator",
    "author": null,
    "author_email": "Marius Kavaliauskas <mariuskava+qlsq@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/02/80/6f863253f7d00798ccbebf1a117efe8c8ea7afad9891217204ef2e58adfe/qlsq-0.5.0.tar.gz",
    "platform": null,
    "description": "# qlsq (QL\u00b2) - Predictable SQL Query Generation\n\n[![PyPI version](https://badge.fury.io/py/qlsq.svg)](https://pypi.org/project/qlsq/)\n\n**qlsq** (QL-squared) is a Python library for generating predictable, secure SQL queries from Lisp-like query expressions. It helps you build complex database queries while maintaining control over performance and security.\n\n## \u2728 Key Features\n\n- **Prevents N+1 Query Problems** - Results nesing is not allowed\n- **Query Complexity Control** - Restrict filtering to indexed columns only\n- **Smart Join Management** - Only performs LEFT JOINs for actually selected fields\n- **Claim-Based Access Control** - Fine-grained permissions for each field (read/edit/filter)\n- **Predictable Output** - Generates clean, parameterized SQL queries\n- **PostgreSQL Integration** - Works seamlessly with psycopg, generating parameterized queries\n\n## \ud83d\ude80 Installation\n\n```bash\npip install qlsq\n# or\nuv add qlsq\n# or\npoetry add qlsq\n```\n\n## \ud83d\udccb Requirements\n\n- Python 3.12+\n- psycopg2 or psycopg3 (for PostgreSQL integration)\n\n## \ud83c\udfaf Use Cases\n\nPerfect for applications that need:\n- Dynamic query building from frontend filters\n- Multi-tenant applications with complex permissions\n- APIs that expose flexible data querying capabilities\n- Applications requiring predictable query performance\n\n## \ud83d\udcd6 Quick Start\n\n### 1. Define Your Context\n\nEvery query operates within a context that defines tables and fields:\n\n```python\nfrom qlsq import ContextTable, ContextField, Context, QueryType\n\n# Define tables and their relationships\ntables = [\n    ContextTable(\n        alias=\"ut\",\n        source=\"user_tasks\", \n        join_condition=None,  # Root table\n        depends_on=[]\n    ),\n    ContextTable(\n        alias=\"u\",\n        source=\"users\",\n        join_condition=\"u.id = ut.user_id\",\n        depends_on=[\"ut\"]  # Depends on user_tasks table\n    ),\n]\n\n# Define available fields with permissions\nfields = [\n    ContextField(\n        alias=\"full_name\",\n        source=\"full_name\",\n        query_type=QueryType.text,\n        table_alias=\"u\",\n        read_claim=\"r_full_name\",\n        edit_claim=\"e_full_name\", \n        filter_claim=\"f_full_name\",\n    ),\n    ContextField(\n        alias=\"user_id\",\n        source=\"user_id\",\n        query_type=QueryType.numeric,\n        table_alias=\"ut\",\n        read_claim=\"r_user_id\",\n        edit_claim=\"e_user_id\",\n        filter_claim=\"f_user_id\",\n    ),\n]\n\n# Create the context\ncontext = Context(tables, fields)\n\n# Create the context registry\ncontext_registry = ContextRegistry(\n    {\n        \"user_tasks\": context,\n    }\n)\n```\n\n### 2. Write Lisp-Like Queries\n\n```python\n# Simple query: SELECT full_name WHERE user_id = 3\nquery_expression = [\n    [\"using\", \"user_tasks\"], # specify context\n    [\"select\", \"full_name\"],\n    [\"where\", [\"eq\", \"user_id\", 3]]\n]\n```\n\n### 3. Generate SQL (Two Approaches)\n\n**Approach A: Parse then generate**\n```python\n# Parse and generate SQL\nquery = context.parse_query(query_expression)\nsql, params = query.to_sql()\n\nprint(\"Generated SQL:\")\nprint(sql)\n# Output: SELECT u.full_name FROM user_tasks ut LEFT JOIN users u ON u.id = ut.user_id WHERE (ut.user_id = %(param_0)s);\n\nprint(\"Parameters:\")\nprint(params)\n# Output: {\"param_0\": 3}\n```\n\n**Approach B: Direct generation with claims**\n```python\n# Generate SQL directly with claims validation\nuser_claims = [\"r_full_name\", \"f_user_id\"]  # User's permissions\nsql, params = context.to_sql(query_expression, user_claims)\n```\n\n### 4. Claims-Based Security\n\n```python\n# Define user permissions\nuser_claims = [\"r_full_name\", \"f_user_id\"]  # Can read full_name, filter by user_id\n\n# This will work - user has required claims\nquery = context.parse_query([[\"select\", \"full_name\"], [\"where\", [\"eq\", \"user_id\", 3]]])\nquery.assert_claims(user_claims)  # Validates permissions\n\n# This will fail - user lacks r_user_id claim\ntry:\n    query = context.parse_query([[\"select\", \"user_id\"]])\n    query.assert_claims(user_claims)  # Raises MissingClaimsError\nexcept MissingClaimsError as e:\n    print(f\"Access denied: {e}\")\n```\n\n### 5. Execute with psycopg\n\n```python\nimport psycopg2\n\n# Execute the query\nwith psycopg2.connect(database_url) as conn:\n    with conn.cursor() as cursor:\n        cursor.execute(sql, params)\n        results = cursor.fetchall()\n```\n\n## \ud83d\udd0d Advanced Examples\n\n### Complex Filtering\n\n```python\n# Multiple conditions with AND/OR logic\nquery = [\n    [\"using\", \"user_tasks\"],\n    [\"select\", \"full_name\", \"user_id\"],\n    [\"where\", [\n        \"and\",\n        [\"eq\", \"user_id\", 3],\n        [\"like\", \"full_name\", [\"str\", \"%john%\"]]\n    ]]\n]\n```\n\n### Mathematical Operations\n\n```python\n# Arithmetic operations\nquery = [\n    [\"select\", [\"add\", \"field1\", \"field2\"]],  # Addition\n    [\"select\", [\"sub\", \"field1\", \"field2\"]],  # Subtraction  \n    [\"select\", [\"mul\", \"field1\", \"field2\"]],  # Multiplication\n    [\"select\", [\"div\", \"field1\", \"field2\"]],  # Division\n]\n```\n\n### String Operations\n\n```python\n# String manipulation\nquery = [\n    [\"select\", [\"concat\", \"first_name\", [\"str\", \" \"], \"last_name\"]],  # Concatenation\n    [\"select\", [\"lower\", \"full_name\"]],   # Lowercase\n    [\"select\", [\"upper\", \"full_name\"]],   # Uppercase\n]\n```\n\n### Date Handling\n\n```python\n# Date operations\nquery = [\n    [\"where\", [\"eq\", \"created_at\", [\"date\", \"2024-01-15T10:30:00Z\"]]]\n]\n```\n\n### Null Checks and Coalescing\n\n```python\n# Working with NULL values\nquery = [\n    [\"select\", [\"coalesce\", \"nickname\", [\"str\", \"-NA-\"]]],\n    [\"where\", [\"is_not_null\", \"email\"]],  # Check for non-null\n]\n```\n\n### Sorting and Limiting\n\n```python\n# Add sorting and pagination\nquery = [\n    [\"using\", \"users\"],\n    [\"select\", \"full_name\", \"user_id\"],\n    [\"where\", [\"gt\", \"user_id\", 0]],\n    [\"orderby\", [\"asc\", \"full_name\"]],  # Note: asc/desc wraps the field\n    [\"limit\", 10],\n    [\"offset\", 20]\n]\n```\n\n### IN Clause and Complex Conditions\n\n```python\n# Multiple values and complex logic\nquery = [\n    [\"using\", \"users\"],\n    [\"select\", \"full_name\"],\n    [\"where\", [\n        \"or\",\n        [\"in\", \"user_id\", 1, 2, 3, 4],\n        [\"and\", \n            [\"gte\", \"age\", 18],\n            [\"like\", \"email\", [\"str\", \"%@company.com\"]]\n        ]\n    ]]\n]\n```\n\n## \ud83d\udee1\ufe0f Security Features\n\n### Claim-Based Access Control\n\n```python\n# Only users with proper claims can access fields\nuser_claims = [\"r_full_name\", \"f_user_id\"]  # Can read full_name, filter by user_id\nsql, params = context.to_sql(query_expression, user_claims) # Will raise MissingClaimsError if claims are missing\n```\n\n### Query Validation\n\n- Prevents filtering on non-indexed columns (if configured)\n- Validates field access permissions\n- Ensures proper table relationships\n- Protects against SQL injection through parameterization\n\n## \ud83c\udfa8 Query Language Reference\n\n### Core Operations\n| Operation | Syntax | Example |\n|-----------|--------|---------|\n| Select | `[\"select\", \"field1\", \"field2\"]` | `[\"select\", \"name\", \"email\"]` |\n| Where | `[\"where\", condition]` | `[\"where\", [\"eq\", \"id\", 1]]` |\n\n### Comparison Operators\n| Operation | Syntax | Example |\n|-----------|--------|---------|\n| Equals | `[\"eq\", field, value]` | `[\"eq\", \"status\", [\"str\", \"active\"]]` |\n| Not Equals | `[\"neq\", field, value]` | `[\"neq\", \"status\", [\"str\", \"deleted\"]]` |\n| Greater Than | `[\"gt\", field, value]` | `[\"gt\", \"age\", 18]` |\n| Greater/Equal | `[\"gte\", field, value]` | `[\"gte\", \"score\", 100]` |\n| Less Than | `[\"lt\", field, value]` | `[\"lt\", \"price\", 50]` |\n| Less/Equal | `[\"lte\", field, value]` | `[\"lte\", \"quantity\", 10]` |\n| Like Pattern | `[\"like\", field, pattern]` | `[\"like\", \"name\", [\"str\", \"%john%\"]]` |\n| In List | `[\"in\", field, val1, val2, ...]` | `[\"in\", \"id\", 1, 2, 3]` |\n\n### Logical Operators\n| Operation | Syntax | Example |\n|-----------|--------|---------|\n| And | `[\"and\", cond1, cond2, ...]` | `[\"and\", [\"eq\", \"a\", 1], [\"eq\", \"b\", 2]]` |\n| Or | `[\"or\", cond1, cond2, ...]` | `[\"or\", [\"eq\", \"status\", \"active\"], [\"eq\", \"status\", \"pending\"]]` |\n| Not | `[\"not\", condition]` | `[\"not\", [\"eq\", \"deleted\", true]]` |\n\n### Null Checks\n| Operation | Syntax | Example |\n|-----------|--------|---------|\n| Is Null | `[\"is_null\", field]` | `[\"is_null\", \"deleted_at\"]` |\n| Is Not Null | `[\"is_not_null\", field]` | `[\"is_not_null\", \"email\"]` |\n\n### Mathematical Operations\n| Operation | Syntax | Example |\n|-----------|--------|---------|\n| Addition | `[\"add\", expr1, expr2, ...]` | `[\"add\", \"base_price\", \"tax\"]` |\n| Subtraction | `[\"sub\", expr1, expr2, ...]` | `[\"sub\", \"total\", \"discount\"]` |\n| Multiplication | `[\"mul\", expr1, expr2, ...]` | `[\"mul\", \"price\", \"quantity\"]` |\n| Division | `[\"div\", expr1, expr2]` | `[\"div\", \"total\", \"count\"]` |\n\n### String Operations\n| Operation | Syntax | Example |\n|-----------|--------|---------|\n| Concatenate | `[\"concat\", str1, str2, ...]` | `[\"concat\", \"first_name\", [\"str\", \" \"], \"last_name\"]` |\n| Lowercase | `[\"lower\", string_expr]` | `[\"lower\", \"email\"]` |\n| Uppercase | `[\"upper\", string_expr]` | `[\"upper\", \"code\"]` |\n| Coalesce | `[\"coalesce\", expr1, expr2, ...]` | `[\"coalesce\", \"nickname\", [\"str\", \"-NA-\"]]` |\n\n### Literal Values\n| Type | Syntax | Example |\n|------|--------|---------|\n| String | `[\"str\", \"value\"]` | `[\"str\", \"hello world\"]` |\n| Date | `[\"date\", \"iso_string\"]` | `[\"date\", \"2024-01-15T10:30:00Z\"]` |\n| Integer | `42` | `[\"eq\", \"age\", 25]` |\n| Float | `3.14` | `[\"eq\", \"price\", 19.99]` |\n| Boolean | `true`/`false` | `[\"eq\", \"active\", true]` |\n| Null | `null` | `[\"eq\", \"deleted_at\", null]` |\n\n### Ordering and Pagination\n| Operation | Syntax | Example |\n|-----------|--------|---------|\n| Order By | `[\"orderby\", direction1, direction2, ...]` | `[\"orderby\", [\"asc\", \"name\"], [\"desc\", \"created_at\"]]` |\n| Ascending | `[\"asc\", field]` | `[\"asc\", \"name\"]` |\n| Descending | `[\"desc\", field]` | `[\"desc\", \"created_at\"]` |\n| Limit | `[\"limit\", count]` | `[\"limit\", 10]` |\n| Offset | `[\"offset\", count]` | `[\"offset\", 20]` |\n\n## \u26a0\ufe0f Limitations\n\n- **No nested queries** - Complex nesting must be implemented in SQL\n- **PostgreSQL only** - Currently only supports PostgreSQL via psycopg\n- **Left joins only** - Developer must ensure there are no unwanted duplicates\n\n## \ud83d\udd27 API Reference\n\n`FIXME`: list main classes, methods, exceptions\n\n\n## \ud83e\udd1d Contributing\n\nContributions are welcome! `main` branch is for development, each release and subseaquent hotfixes land on separate branches like `v0.1.3`.\n\n## \ud83d\udcc4 License\n\nThis project is licensed under the MIT License - see the LICENSE file for details.\n\n## \ud83d\udd17 Links\n\n- [PyPI Package](https://pypi.org/project/qlsq/)\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "dynamic sql generetor",
    "version": "0.5.0",
    "project_urls": {
        "Homepage": "https://gitlab.com/qlsq/qlsq",
        "Issue Tracker": "https://gitlab.com/qlsq/qlsq/-/issues",
        "Source Code": "https://gitlab.com/qlsq/qlsq"
    },
    "split_keywords": [
        "sql",
        " postgresql",
        " sql-generator"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "137dd04952370491ecaf5b3fdaf82e9fe8dc30d496393542ccf407770dc4b6b9",
                "md5": "4eae31c9573565270e1e01db2d716ec3",
                "sha256": "defa19c14d4c06ad1ee4d965a2efe8be39fb0462872f92b29a8bf70014a7116f"
            },
            "downloads": -1,
            "filename": "qlsq-0.5.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "4eae31c9573565270e1e01db2d716ec3",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.12",
            "size": 11022,
            "upload_time": "2025-07-17T19:23:09",
            "upload_time_iso_8601": "2025-07-17T19:23:09.466072Z",
            "url": "https://files.pythonhosted.org/packages/13/7d/d04952370491ecaf5b3fdaf82e9fe8dc30d496393542ccf407770dc4b6b9/qlsq-0.5.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "02806f863253f7d00798ccbebf1a117efe8c8ea7afad9891217204ef2e58adfe",
                "md5": "7526a6c3f8c685f8b233c7d28e16fd50",
                "sha256": "064e43470fbda9465ee1570dd8ebcf96408387c02bc961b60214334afed45a3f"
            },
            "downloads": -1,
            "filename": "qlsq-0.5.0.tar.gz",
            "has_sig": false,
            "md5_digest": "7526a6c3f8c685f8b233c7d28e16fd50",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.12",
            "size": 14822,
            "upload_time": "2025-07-17T19:23:10",
            "upload_time_iso_8601": "2025-07-17T19:23:10.981132Z",
            "url": "https://files.pythonhosted.org/packages/02/80/6f863253f7d00798ccbebf1a117efe8c8ea7afad9891217204ef2e58adfe/qlsq-0.5.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-07-17 19:23:10",
    "github": false,
    "gitlab": true,
    "bitbucket": false,
    "codeberg": false,
    "gitlab_user": "qlsq",
    "gitlab_project": "qlsq",
    "lcname": "qlsq"
}
        
Elapsed time: 1.43042s