Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 70 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ rdf4j-python bridges the gap between Python and the robust [Eclipse RDF4J](https
- **Async-First Design**: Native support for async/await with synchronous fallback
- **Repository Management**: Create, access, and manage RDF4J repositories programmatically
- **SPARQL Support**: Execute SELECT, ASK, CONSTRUCT, and UPDATE queries effortlessly
- **SPARQL Query Builder**: Fluent, programmatic query construction with method chaining
- **Transaction Support**: Atomic operations with commit/rollback and isolation levels
- **Flexible Data Handling**: Add, retrieve, and manipulate RDF triples and quads
- **File Upload**: Upload RDF files (Turtle, N-Triples, N-Quads, RDF/XML, JSON-LD, TriG, N3) directly to repositories
Expand Down Expand Up @@ -88,6 +89,62 @@ if __name__ == "__main__":
asyncio.run(main())
```

### SPARQL Query Builder

Build queries programmatically with method chaining instead of writing raw SPARQL strings:

```python
from rdf4j_python import select, ask, construct, describe, GraphPattern, Namespace

ex = Namespace("ex", "http://example.org/")
foaf = Namespace("foaf", "http://xmlns.com/foaf/0.1/")

# SELECT with typed terms — IRIs serialize automatically
query = (
select("?person", "?name")
.where("?person", foaf.type, ex.Person)
.where("?person", foaf.name, "?name")
.optional("?person", foaf.email, "?email")
.filter("?name != 'Bob'")
.order_by("?name")
.limit(10)
.build()
)

# Or use string-based prefixed names
query = (
select("?name")
.prefix("foaf", "http://xmlns.com/foaf/0.1/")
.where("?person", "a", "foaf:Person")
.where("?person", "foaf:name", "?name")
.build()
)

# GROUP BY with aggregation
query = (
select("?city", "(COUNT(?person) AS ?count)")
.where("?person", ex.city, "?city")
.group_by("?city")
.having("COUNT(?person) > 1")
.order_by("DESC(?count)")
.build()
)

# ASK, CONSTRUCT, and DESCRIBE
ask_query = ask().where("?s", ex.name, "?name").build()

construct_query = (
construct(("?s", ex.fullName, "?name"))
.where("?s", ex.firstName, "?fname")
.bind("CONCAT(?fname, ' ', ?lname)", "?name")
.build()
)

describe_query = describe(ex.alice).build()
```

The query builder supports FILTER, OPTIONAL, UNION, BIND, VALUES, sub-queries, DISTINCT, ORDER BY, GROUP BY, HAVING, LIMIT, and OFFSET. Both raw strings and typed objects (`IRI`, `Variable`, `Literal`, `Namespace`) work as terms.

### Working with Multiple Graphs

```python
Expand Down Expand Up @@ -155,15 +212,18 @@ async def advanced_example():
]
await repo.add_statements(statements)

# Complex SPARQL query
query = """
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?name ?email WHERE {
?person foaf:name ?name .
OPTIONAL { ?person foaf:email ?email }
}
ORDER BY ?name
"""
# Query with the fluent query builder
from rdf4j_python import select
from rdf4j_python.model._namespace import Namespace

foaf = Namespace("foaf", "http://xmlns.com/foaf/0.1/")
query = (
select("?name", "?email")
.where("?person", foaf.name, "?name")
.optional("?person", foaf.email, "?email")
.order_by("?name")
.build()
)
results = await repo.query(query)
```

Expand Down Expand Up @@ -256,6 +316,7 @@ For more detailed examples, see the [examples](examples/) directory.
rdf4j_python/
├── _driver/ # Core async driver implementation
├── model/ # Data models and configurations
├── query/ # SPARQL query builder
├── exception/ # Custom exceptions
└── utils/ # Utility functions

Expand Down
10 changes: 6 additions & 4 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,15 @@ python examples/delete_repository.py
### 🔍 Data Querying Examples

#### `query_and_print.py`
Comprehensive example of SPARQL querying and result formatting:
Comprehensive example of SPARQL querying and result formatting using the fluent query builder:
- SELECT queries with various clauses (FILTER, OPTIONAL, JOIN)
- CONSTRUCT queries for data transformation
- ASK queries for boolean checks
- Aggregate queries (COUNT, AVG, MIN, MAX)
- Multiple result formatting options

**Key Features:**
- Fluent SPARQL query builder (no raw query strings)
- Automatic test data setup
- Formatted table output
- JSON-like result formatting
Expand All @@ -92,8 +93,8 @@ Comprehensive example of SPARQL querying and result formatting:
python examples/query_and_print.py
```

#### `query.py` (Original)
Simple query example showing basic SPARQL execution.
#### `query.py`
Simple query example showing basic SPARQL execution using the query builder.

**Usage:**
```bash
Expand All @@ -112,6 +113,7 @@ End-to-end example demonstrating the full repository lifecycle:

**Key Features:**
- Multi-repository workflow
- Fluent SPARQL query builder with Namespace objects
- Real-world data scenarios (customers, products, analytics)
- Named graph usage
- Comprehensive error handling and cleanup
Expand All @@ -121,7 +123,7 @@ End-to-end example demonstrating the full repository lifecycle:
python examples/complete_workflow.py
```

#### `repo.py` (Original)
#### `repo.py`
Basic repository creation and data insertion example.

**Usage:**
Expand Down
54 changes: 29 additions & 25 deletions examples/complete_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@

import asyncio

from rdf4j_python import AsyncRdf4j
from rdf4j_python import AsyncRdf4j, select
from rdf4j_python.model._namespace import Namespace
from rdf4j_python.model.repository_config import (
MemoryStoreConfig,
RepositoryConfig,
SailRepositoryConfig,
)
from rdf4j_python.model.term import IRI, Literal, Quad

# Define namespaces for query building
ecom = Namespace("ecom", "http://example.com/")


async def workflow_step_1_create_repositories():
"""Step 1: Create multiple repositories with different configurations."""
Expand Down Expand Up @@ -199,14 +203,14 @@ async def workflow_step_3_query_data():
# Query 1: Customer information
print("👥 Customer Information:")
customer_repo = await db.get_repository("customer-data")
customer_query = """
SELECT ?customer ?name ?email ?age WHERE {
?customer <http://example.com/name> ?name .
OPTIONAL { ?customer <http://example.com/email> ?email }
OPTIONAL { ?customer <http://example.com/age> ?age }
}
ORDER BY ?name
"""
customer_query = (
select("?customer", "?name", "?email", "?age")
.where("?customer", ecom.name, "?name")
.optional("?customer", ecom.email, "?email")
.optional("?customer", ecom.age, "?age")
.order_by("?name")
.build()
)
customer_results = await customer_repo.query(customer_query)
for result in customer_results:
name = result["name"].value if result["name"] else "N/A"
Expand All @@ -217,14 +221,14 @@ async def workflow_step_3_query_data():
# Query 2: Product catalog
print("\n🛍️ Product Catalog:")
product_repo = await db.get_repository("product-catalog")
product_query = """
SELECT ?product ?name ?price ?category WHERE {
?product <http://example.com/name> ?name .
OPTIONAL { ?product <http://example.com/price> ?price }
OPTIONAL { ?product <http://example.com/category> ?category }
}
ORDER BY ?price
"""
product_query = (
select("?product", "?name", "?price", "?category")
.where("?product", ecom.name, "?name")
.optional("?product", ecom.price, "?price")
.optional("?product", ecom.category, "?category")
.order_by("?price")
.build()
)
product_results = await product_repo.query(product_query)
for result in product_results:
name = result["name"].value if result["name"] else "N/A"
Expand All @@ -235,14 +239,14 @@ async def workflow_step_3_query_data():
# Query 3: Purchase analytics
print("\n📊 Purchase Analytics:")
analytics_repo = await db.get_repository("analytics-data")
analytics_query = """
SELECT ?purchase ?customer ?product ?date WHERE {
?purchase <http://example.com/customer> ?customer .
?purchase <http://example.com/product> ?product .
OPTIONAL { ?purchase <http://example.com/date> ?date }
}
ORDER BY ?date
"""
analytics_query = (
select("?purchase", "?customer", "?product", "?date")
.where("?purchase", ecom.customer, "?customer")
.where("?purchase", ecom.product, "?product")
.optional("?purchase", ecom.date, "?date")
.order_by("?date")
.build()
)
analytics_results = await analytics_repo.query(analytics_query)
for result in analytics_results:
customer = result["customer"].value if result["customer"] else "N/A"
Expand Down
8 changes: 6 additions & 2 deletions examples/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pyoxigraph import QuerySolutions

from rdf4j_python import AsyncRdf4j
from rdf4j_python import AsyncRdf4j, select
from rdf4j_python.model.term import IRI, Literal, Quad


Expand All @@ -25,7 +25,11 @@ async def main():
),
]
)
result = await repo.query("SELECT * WHERE { ?s ?p ?o }")

# Build the query using the fluent query builder
query = select("?s", "?p", "?o").where("?s", "?p", "?o").build()

result = await repo.query(query)
assert isinstance(result, QuerySolutions)
for solution in result:
print(solution)
Expand Down
Loading