Skip to content

Instantly share code, notes, and snippets.

@wizpig64
Last active December 15, 2022 18:07
Show Gist options
  • Save wizpig64/419c78739c40c552c81881b59983d511 to your computer and use it in GitHub Desktop.
Save wizpig64/419c78739c40c552c81881b59983d511 to your computer and use it in GitHub Desktop.
django management command to dump API metadata
import json
from pathlib import Path
from django.core.management.base import BaseCommand
from django.utils.module_loading import import_string
from rest_framework.fields import Field
from rest_framework.metadata import SimpleMetadata
from rest_framework.routers import BaseRouter as Router
from rest_framework.schemas.openapi import AutoSchema
class ExtendedMetadata(SimpleMetadata):
"""Adds additional field metadata that can be useful for building front ends."""
extra_validators = ["format", "pattern"]
def get_field_info(self, field: Field):
field_info = super().get_field_info(field)
# add extra validators from the OpenAPI schema generator.
schema = {}
AutoSchema()._map_field_validators(field, schema)
field_info.update(
(validator, schema[validator])
for validator in self.extra_validators
if validator in schema
)
# add additional data from serializer.
field_info["initial"] = field.initial
field_info["field_name"] = field.field_name
field_info["write_only"] = field.write_only
return field_info
class Command(BaseCommand):
"""
Dumps a DRF Router's field options into JSON files.
This command was inspired by John Franey of NepFin Engineering:
https://medium.com/nepfin-engineering/d103cf416e23
Requires you to have a DRF router instance defined somewhere in your project.
"""
help = "Dump API metadata options into json files."
metadata_generator = ExtendedMetadata()
def add_arguments(self, parser):
parser.add_argument(
"router",
nargs="?",
default="common.api.router",
type=import_string,
help="Dotted import path to a DRF router instance.",
)
parser.add_argument(
"dump_dir",
nargs="?",
default="common/js/options/",
type=Path,
help="The directory into which options shall be dumped.",
)
def handle(self, router: Router, dump_dir: Path, **options):
# iterate through the router's registry, looking for serializers.
known_dump_files = []
for prefix, viewset, basename in router.registry:
serializer_class = self.get_serializer_class(viewset)
# if no serializer found, skip this viewset.
if serializer_class is None:
self.stdout.write(f"Skipping {viewset.__name__}, no serializer found.")
continue
# ensure dump_dir exists, just before the first dump.
if not known_dump_files:
dump_dir.mkdir(parents=True, exist_ok=True)
# dump extended viewset options into a json file.
dump_file = dump_dir / (prefix + ".json")
known_dump_files.append(dump_file)
self.stdout.write(f"Dumping options for {viewset.__name__} to {dump_file}")
metadata = self.metadata_generator.get_serializer_info(serializer_class())
dump_file.write_text(json.dumps(metadata, indent=2, sort_keys=True))
# avoid printing *nothing* to the console.
if not known_dump_files:
self.stdout.write(f"No serializers found, no options dumped.")
# alert user if any .json files exist in dump_dir that we don't know about.
if dump_dir.exists():
for file in dump_dir.iterdir():
if file.suffix == ".json" and file not in known_dump_files:
self.stdout.write(f"Warning: Unknown options {file} exists!")
@staticmethod
def get_serializer_class(viewset):
"""Get the viewset's serializer in a robust way."""
view = viewset()
# this code was originally from rest_framework/filters.py, lines 209-217:
if hasattr(view, "get_serializer_class"):
try:
return view.get_serializer_class()
except AssertionError:
# Raised by the default implementation if
# no serializer_class was found
return None
else:
return getattr(view, "serializer_class", None)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment