A lightweight Python GraphQL server framework with automatic resolver mapping and schema introspection
Project description
pgql
A lightweight Python GraphQL server framework with automatic resolver mapping, schema introspection, and built-in support for Starlette/Uvicorn.
Features
- ๐ Automatic Resolver Mapping: Map Python class methods to GraphQL fields based on return types
- ๐ Recursive Schema Loading: Organize your
.gqlschema files in nested directories - ๐ Built-in Introspection: Full GraphQL introspection support out of the box
- ๐ฏ Instance-based Resolvers: Use class instances for stateful resolvers with dependency injection
- โก Async Support: Built on Starlette and Uvicorn for high-performance async handling
- ๐ง YAML Configuration: Simple YAML-based server configuration
- ๐ฆ Type Support: Full support for
extend type, nested types, and GraphQL type modifiers - ๐ Authorization System: Intercept resolver calls with
on_authorizefunction - ๐ช Session Management: Built-in session store with automatic cookie handling
Installation
pip install pgql
Quick Start
1. Define Your GraphQL Schema
Create your schema files in a directory structure:
schema/
โโโ schema.gql
โโโ user/
โโโ types.gql
โโโ queries.gql
schema/schema.gql:
schema {
query: Query
}
schema/user/types.gql:
type User {
id: ID!
name: String!
email: String!
}
schema/user/queries.gql:
extend type Query {
getUser(id: ID!): User!
getUsers: [User!]!
}
2. Create Resolver Classes
# resolvers/user.py
class User:
def getUser(self, parent, info, id):
# Your logic here
return {'id': id, 'name': 'John Doe', 'email': 'john@example.com'}
def getUsers(self, parent, info):
return [
{'id': 1, 'name': 'John', 'email': 'john@example.com'},
{'id': 2, 'name': 'Jane', 'email': 'jane@example.com'}
]
3. Configure Server
config.yml:
http_port: 8080
debug: true
server:
host: localhost
routes:
- mode: gql
endpoint: /graphql
schema: schema # Path to schema directory
4. Start Server
from pgql import HTTPServer
from resolvers.user import User
# Create resolver instances
user_resolver = User()
# Initialize server
server = HTTPServer('config.yml')
# Map resolvers to GraphQL types
server.gql({
'User': user_resolver
})
# Start server
server.start()
5. Query Your API
curl -X POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ getUsers { id name email } }"}'
Response:
{
"data": {
"getUsers": [
{"id": "1", "name": "John", "email": "john@example.com"},
{"id": "2", "name": "Jane", "email": "jane@example.com"}
]
}
}
How It Works
Automatic Resolver Mapping
pgql automatically maps resolver methods to GraphQL fields based on return types:
- If
Query.getUserreturns typeUser, pgql looks for a method namedgetUserin theUserresolver class - The mapping works recursively for nested types (e.g.,
User.companyโCompany.company)
Example:
type User {
id: ID!
company: Company!
}
type Company {
id: ID!
name: String!
}
type Query {
getUser: User!
}
class User:
def getUser(self, parent, info):
return {'id': 1, 'company': {'id': 1}}
class Company:
def company(self, parent, info):
# parent contains the User object
company_id = parent['id']
return {'id': company_id, 'name': 'Acme Corp'}
# Register both resolvers
server.gql({
'User': User(),
'Company': Company()
})
Resolver Arguments
All resolver methods receive:
self: The resolver instance (for stateful resolvers)parent: The parent object from the previous resolverinfo: GraphQL execution info (field name, context, variables, etc.)**kwargs: Field arguments from the query
def getUser(self, parent, info, id):
# id comes from query arguments
return fetch_user(id)
Introspection
pgql supports full GraphQL introspection out of the box:
curl -X POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { queryType { name } } }"}'
This works with tools like:
- GraphiQL
- GraphQL Playground
- Apollo Studio
- Postman
Advanced Usage
Authorization Interceptor
pgql allows you to intercept every resolver call to implement authorization logic using on_authorize:
from pgql import HTTPServer, AuthorizeInfo
def on_authorize(auth_info: AuthorizeInfo) -> bool:
"""
Intercept every resolver call for authorization
Args:
auth_info.operation: 'query', 'mutation', or 'subscription'
auth_info.src_type: Parent GraphQL type invoking the resolver (e.g., 'User' for User.company)
auth_info.dst_type: GraphQL type being executed (e.g., 'Company' for User.company)
auth_info.resolver: Field/resolver name (e.g., 'getUser', 'company')
auth_info.session_id: Session ID from cookie (None if not present)
Returns:
True to allow execution, False to deny
"""
# Deny access if no session
if not auth_info.session_id:
return False
# Restrict specific field access based on parent type
if auth_info.src_type == "User" and auth_info.resolver == "company":
return auth_info.session_id == "admin123" # Only admin can access User.company
return True
server = HTTPServer('config.yml')
server.on_authorize(on_authorize) # Register authorization function
server.gql({...})
Session Management:
pgql extracts session_id from cookies automatically. Set the cookie in your client:
curl -X POST http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Cookie: session_id=abc123" \
-d '{"query": "{ getUsers { id } }"}'
Authorization Flow Example:
When querying { getUser { id company { name } } }:
- First call:
Query.getUser โ User(src_type='Query', dst_type='User', resolver='getUser') - Second call:
User.company โ Company(src_type='User', dst_type='Company', resolver='company')
Note: The on_authorize function is optional. If not set, all resolvers execute without authorization checks.
Session Management
pgql includes a built-in session store for managing user sessions:
from pgql import HTTPServer, Session
server = HTTPServer('config.yml')
# Create a new session
session = server.create_session(max_age=3600) # 1 hour
# Store any data in the session
session.set('user_id', 123)
session.set('username', 'john')
session.set('roles', ['admin', 'user'])
session.set('preferences', {'theme': 'dark'})
# Retrieve session
session = server.get_session(session_id)
user_id = session.get('user_id')
# Delete session (logout)
server.delete_session(session_id)
Using Sessions in Resolvers:
class UserResolver:
def __init__(self, server):
self.server = server
def login(self, parent, info, username, password):
# Create session on successful login
session = self.server.create_session(max_age=7200)
session.set('user_id', 123)
session.set('authenticated', True)
# Mark session to set cookie in response
info.context['new_session'] = session
return {'success': True, 'session_id': session.session_id}
def getUser(self, parent, info):
# Access session data
session = info.context.get('session')
if session and session.get('authenticated'):
return {'id': session.get('user_id'), 'name': 'John'}
return None
Configure cookie name in YAML:
http_port: 8080
cookie_name: my_session_id # Custom cookie name
server:
host: localhost
routes:
- mode: gql
endpoint: /graphql
schema: schema
For complete session documentation, see SESSIONS.md.
Note: The on_authorize function is optional. If not set, all resolvers execute without authorization checks.
Nested Schema Organization
Organize your schemas by domain:
schema/
โโโ schema.gql
โโโ user/
โ โโโ types.gql
โ โโโ queries.gql
โ โโโ mutations.gql
โ โโโ inputs.gql
โโโ company/
โโโ types.gql
โโโ queries.gql
pgql recursively loads all .gql files.
Multiple Routes
Configure multiple GraphQL endpoints:
server:
routes:
- mode: gql
endpoint: /graphql
schema: schema
- mode: gql
endpoint: /admin/graphql
schema: admin_schema
Requirements
- Python >= 3.8
- graphql-core >= 3.2.0
- starlette >= 0.27.0
- uvicorn >= 0.23.0
- pyyaml >= 6.0
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Links
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pgql-0.2.0.tar.gz.
File metadata
- Download URL: pgql-0.2.0.tar.gz
- Upload date:
- Size: 13.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6962791585044ec7ea52f56a59b20a3428921aedc6d41957818246e040a0bd4b
|
|
| MD5 |
4aefabd69f1a9e2d859f4ac3ab3b81c8
|
|
| BLAKE2b-256 |
5ca71a96d58992d75728b960330632e8ed8d706e5dd2744a0004ff0e447bddf8
|
File details
Details for the file pgql-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pgql-0.2.0-py3-none-any.whl
- Upload date:
- Size: 11.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
51fc48a336018f63c998e10f2345dc9b8f51497e12c16545f74891ebc1394bb2
|
|
| MD5 |
f38334af5264ed39aaf22ed186174a31
|
|
| BLAKE2b-256 |
846760da42006d3852558456ba10b7257259384a4c85792c03976e90dbca7090
|