Skip to main content

Bringing OData Standards to Django - A comprehensive package implementing OData v4 specification for REST APIs with powerful querying capabilities and enterprise-grade functionality

Project description

Django OData

Bringing OData Standards to Django - A comprehensive Django package that implements the OData (Open Data Protocol) specification for REST APIs, enabling standardized data access patterns with powerful querying capabilities.

This package transforms your Django models into OData-compliant endpoints by seamlessly integrating drf-flex-fields and odata-query, providing enterprise-grade API functionality with minimal configuration.

Features

🎯 OData Specification Compliance

  • Complete OData v4 Query Support: Full implementation of OData query options ($filter, $orderby, $top, $skip, $select, $expand, $count)
  • OData Response Format: Standards-compliant JSON responses with proper @odata.context and metadata annotations
  • Service Metadata: Built-in $metadata endpoint for complete API discovery and client generation
  • OData Error Handling: Standardized error responses following OData specifications

Performance & Optimization

  • Intelligent Query Optimization: Automatic select_related() and prefetch_related() application to prevent N+1 queries
  • Smart Query Translation: OData filter expressions automatically converted to optimized Django ORM queries
  • Efficient Data Loading: Only requested fields are serialized and transmitted

🔧 Developer Experience

  • Minimal Configuration: Transform existing Django models into OData endpoints with just a few lines of code
  • Django REST Framework Integration: Seamlessly extends DRF viewsets and serializers
  • Type Safety: Proper OData-to-Django field type mapping for all Django field types
  • Flexible Architecture: Easy to customize and extend for specific business requirements

Installation

pip install django-odata

Or install from source:

git clone https://github.com/dev-muhammad/django-odata.git
cd django-odata
pip install -e .

Dependencies

  • Django >= 4.2 LTS
  • Python >= 3.8
  • djangorestframework >= 3.12.0
  • drf-flex-fields >= 1.0.0
  • odata-query >= 0.9.0

Note: Django 4.2 LTS is supported until April 2026. Please verify that drf-flex-fields supports Django 4.2 in your environment, as compatibility may vary between versions.

Quick Start

1. Add to INSTALLED_APPS

INSTALLED_APPS = [
    # ... your other apps
    'rest_framework',
    'rest_flex_fields',
    'django_odata',
]

2. Create OData Serializers

from django_odata.serializers import ODataModelSerializer
from .models import BlogPost, Author, Category

class AuthorSerializer(ODataModelSerializer):
    class Meta:
        model = Author
        fields = ['id', 'name', 'email', 'bio']

class CategorySerializer(ODataModelSerializer):
    class Meta:
        model = Category
        fields = ['id', 'name', 'description']

class BlogPostSerializer(ODataModelSerializer):
    class Meta:
        model = BlogPost
        fields = ['id', 'title', 'content', 'status', 'created_at']
        expandable_fields = {
            'author': (AuthorSerializer, {}),
            'categories': (CategorySerializer, {'many': True}),
        }

3. Create OData ViewSets

from django_odata.viewsets import ODataModelViewSet
from .models import BlogPost, Author, Category
from .serializers import BlogPostSerializer, AuthorSerializer, CategorySerializer

class BlogPostViewSet(ODataModelViewSet):
    queryset = BlogPost.objects.all()
    serializer_class = BlogPostSerializer

class AuthorViewSet(ODataModelViewSet):
    queryset = Author.objects.all()
    serializer_class = AuthorSerializer

class CategoryViewSet(ODataModelViewSet):
    queryset = Category.objects.all()
    serializer_class = CategorySerializer

4. Configure URLs

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import BlogPostViewSet, AuthorViewSet, CategoryViewSet

router = DefaultRouter()
router.register(r'posts', BlogPostViewSet)
router.register(r'authors', AuthorViewSet)
router.register(r'categories', CategoryViewSet)

urlpatterns = [
    path('odata/', include(router.urls)),
]

Usage Examples

Basic Queries

# Get all blog posts
GET /odata/posts/

# Get a specific blog post
GET /odata/posts/1/

# Get first 10 posts
GET /odata/posts/?$top=10

# Skip first 20 posts, get next 10
GET /odata/posts/?$skip=20&$top=10

Filtering

# Get published posts
GET /odata/posts/?$filter=status eq 'published'

# Get posts with more than 100 views
GET /odata/posts/?$filter=view_count gt 100

# Get posts created this year
GET /odata/posts/?$filter=year(created_at) eq 2024

# Complex filter
GET /odata/posts/?$filter=status eq 'published' and view_count gt 50

Sorting

# Sort by creation date (newest first)
GET /odata/posts/?$orderby=created_at desc

# Sort by title alphabetically
GET /odata/posts/?$orderby=title asc

# Multiple sort criteria
GET /odata/posts/?$orderby=status desc,created_at desc

Field Selection

# Select specific fields (OData standard)
GET /odata/posts/?$select=id,title,status

# If no $select specified, returns all available fields
GET /odata/posts/

# Omit specific fields (legacy feature)
GET /odata/posts/?omit=content

Field Expansion

# Include author information (automatically adds 'author' to selected fields)
GET /odata/posts/?$expand=author

# Include multiple related fields
GET /odata/posts/?$expand=author,categories

# When using $expand, expanded fields are automatically selected
GET /odata/posts/?$expand=author
# Returns: all fields + author (with expanded data)

# Explicit field selection with expansion
GET /odata/posts/?$select=id,title&$expand=author
# Returns: id, title, author (with expanded data)

# Nested field selection in expanded properties (OData standard)
GET /odata/posts/?$expand=author($select=name,bio)
# Returns: all fields + author (with only name and bio)

# Multiple nested expansions
GET /odata/posts/?$expand=author($select=name,bio),categories($select=id,name)
# Returns: all fields + author (name,bio) + categories (id,name)

# Mixed simple and nested expansions
GET /odata/posts/?$expand=author($select=name),categories,tags($select=name)
# Returns: all fields + author (name only) + categories (all fields) + tags (name only)

# Combine explicit selection with nested expansions
GET /odata/posts/?$select=id,title&$expand=author($select=name,bio)
# Returns: id, title, author (with name and bio only)

Automatic Query Optimization

The package automatically optimizes database queries when using $expand to prevent N+1 query problems:

# This request automatically applies prefetch_related('posts')
GET /odata/authors/?$expand=posts($select=id,title)

# This request automatically applies select_related('author') 
GET /odata/posts/?$expand=author($select=name,bio)

Optimization Rules:

  • Forward relationships (ForeignKey, OneToOne): Uses select_related() for efficient JOINs
  • Reverse relationships (reverse ForeignKey, ManyToMany): Uses prefetch_related() for separate optimized queries
  • No manual optimization needed: The package detects relationship types and applies the appropriate optimization automatically

Counting

# Get total count along with results
GET /odata/posts/?$count=true

# Get count of filtered results
GET /odata/posts/?$filter=status eq 'published'&$count=true

Metadata

# Get service metadata
GET /odata/posts/$metadata

# Get service document
GET /odata/

Advanced Usage

Custom ViewSets

from django_odata.viewsets import ODataModelViewSet

class CustomBlogPostViewSet(ODataModelViewSet):
    queryset = BlogPost.objects.all()
    serializer_class = BlogPostSerializer
    
    def get_queryset(self):
        \"\"\"Add custom filtering logic.\"\"\"
        queryset = super().get_queryset()
        
        # Only show published posts to non-staff users
        if not self.request.user.is_staff:
            queryset = queryset.filter(status='published')
        
        return queryset

Factory Functions

from django_odata.serializers import create_odata_serializer
from django_odata.viewsets import create_odata_viewset

# Create serializer automatically
BlogPostSerializer = create_odata_serializer(
    BlogPost,
    fields=['id', 'title', 'content', 'status'],
    expandable_fields={
        'author': ('myapp.serializers.AuthorSerializer', {}),
    }
)

# Create viewset automatically
BlogPostViewSet = create_odata_viewset(BlogPost, serializer_class=BlogPostSerializer)

Query Builder

from django_odata.utils import ODataQueryBuilder

# Build queries programmatically
query = (ODataQueryBuilder()
         .filter("status eq 'published'")
         .filter("view_count gt 100")
         .order('created_at', desc=True)
         .limit(20)
         .select('id', 'title', 'author')
         .expand('author')
         .build())

# query now contains the query parameters dictionary

OData Query Options Reference

Option Description Example
$filter Filter results based on conditions $filter=status eq 'published'
$orderby Sort results $orderby=created_at desc
$top Limit number of results $top=10
$skip Skip number of results $skip=20
$select Choose specific fields $select=id,title,status
$expand Include related data $expand=author,categories or $expand=author($select=name,bio)
$count Include total count $count=true

Filter Operators

Operator Description Example
eq Equal status eq 'published'
ne Not equal status ne 'draft'
gt Greater than view_count gt 100
ge Greater than or equal rating ge 4.0
lt Less than view_count lt 50
le Less than or equal rating le 3.0
and Logical AND status eq 'published' and featured eq true
or Logical OR status eq 'published' or status eq 'featured'
not Logical NOT not (status eq 'draft')

String Functions

Function Description Example
contains String contains contains(title,'django')
startswith String starts with startswith(title,'How to')
endswith String ends with endswith(title,'Guide')
length String length length(title) gt 10
tolower Convert to lowercase tolower(title) eq 'django guide'
toupper Convert to uppercase toupper(status) eq 'PUBLISHED'

Date Functions

Function Description Example
year Extract year year(created_at) eq 2024
month Extract month month(created_at) eq 12
day Extract day day(created_at) eq 25
hour Extract hour hour(created_at) eq 14
minute Extract minute minute(created_at) eq 30
second Extract second second(created_at) eq 45

Configuration

Add optional settings to your Django settings:

# Optional django-odata settings
DJANGO_ODATA = {
    'SERVICE_ROOT': '/odata/',
    'MAX_PAGE_SIZE': 1000,
    'DEFAULT_PAGE_SIZE': 50,
    'ENABLE_METADATA': True,
    'ENABLE_SERVICE_DOCUMENT': True,
}

Response Format

Collection Response

{
  "@odata.context": "http://example.com/odata/$metadata#posts",
  "@odata.count": 150,
  "value": [
    {
      "id": 1,
      "title": "Introduction to Django",
      "status": "published",
      "author": {
        "id": 1,
        "name": "John Doe",
        "email": "john@example.com"
      }
    }
  ]
}

Single Entity Response

{
  "@odata.context": "http://example.com/odata/$metadata#posts/$entity",
  "id": 1,
  "title": "Introduction to Django",
  "content": "This is a comprehensive guide...",
  "status": "published",
  "created_at": "2024-01-15T10:30:00Z"
}

Error Response

{
  "error": {
    "code": "BadRequest",
    "message": "The query specified in the URI is not valid."
  }
}

Testing

Run the test suite:

# Install test dependencies
pip install -e .[dev]

# Run tests
pytest

# Run tests with coverage
pytest --cov=django_odata

Example Project

See the example/ directory for a complete Django project demonstrating all features:

cd example/
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver

Then visit:

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

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

Credits

Changelog

v0.1.0 (2025-08-30)

  • Initial release
  • Full OData query support ($filter, $orderby, $top, $skip, $select, $expand, $count)
  • Dynamic field selection and expansion
  • Metadata endpoints ($metadata, service document)
  • Comprehensive test suite
  • Example application
  • Support for Django 4.2 LTS and Python 3.8+

v0.1.1 (2025-11-17)

  • Lazy import implemented

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_odata-0.1.1.tar.gz (49.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_odata-0.1.1-py3-none-any.whl (48.7 kB view details)

Uploaded Python 3

File details

Details for the file django_odata-0.1.1.tar.gz.

File metadata

  • Download URL: django_odata-0.1.1.tar.gz
  • Upload date:
  • Size: 49.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_odata-0.1.1.tar.gz
Algorithm Hash digest
SHA256 386be24a722fbe7c232ca7281be77496b35771d1ff6a2dbadaff72ddca2b74d3
MD5 dde7ec73d98b1ec2e72eb5a9b7ac1579
BLAKE2b-256 552d776dd50cb7e0ed9e5912d498857a92b7b36eeb7c1f2c45fd5465269636f6

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_odata-0.1.1.tar.gz:

Publisher: publish-to-pypi.yml on dev-muhammad/django-odata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_odata-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: django_odata-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 48.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_odata-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9a500acbc9a05c1d62cd866dac326c221ff461e5a038f4a03bc5ed58a16e175c
MD5 5323b3d7cbb840332774f4d3a786c55e
BLAKE2b-256 bb0e9875fb11a9bfdd30504d3b547a1e79e4a42f9c80824f88b7ba730e27a4d9

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_odata-0.1.1-py3-none-any.whl:

Publisher: publish-to-pypi.yml on dev-muhammad/django-odata

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page