├── .flake8 ├── .github ├── FUNDING.yml └── workflows │ ├── main.yml │ └── publish.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── css │ └── extra.css ├── index.md ├── parsers.md └── renderers.md ├── mkdocs.yml ├── pyproject.toml ├── requirements.txt ├── rest_framework_yaml ├── __init__.py ├── compat.py ├── encoders.py ├── parsers.py └── renderers.py ├── runtests.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── models.py └── test_renderers.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 79 3 | extend-ignore = E203, E501 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [jpadilla] 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: ["master"] 7 | pull_request: 8 | branches: ["master"] 9 | # Allow rebuilds via API. 10 | repository_dispatch: 11 | types: rebuild 12 | 13 | jobs: 14 | tests: 15 | name: "Python ${{ matrix.python-version }} on ${{ matrix.platform }}" 16 | runs-on: "${{ matrix.platform }}" 17 | env: 18 | USING_COVERAGE: '3.8' 19 | 20 | strategy: 21 | matrix: 22 | platform: ["ubuntu-latest"] 23 | python-version: ["3.5", "3.6", "3.7", "3.8"] 24 | 25 | steps: 26 | - uses: "actions/checkout@v2" 27 | - uses: "actions/setup-python@v1" 28 | with: 29 | python-version: "${{ matrix.python-version }}" 30 | - name: "Install dependencies" 31 | run: | 32 | python -VV 33 | python -m site 34 | python -m pip install --upgrade pip setuptools wheel 35 | python -m pip install --upgrade virtualenv tox tox-gh-actions 36 | 37 | - name: "Run tox targets for ${{ matrix.python-version }}" 38 | run: "python -m tox" 39 | env: 40 | PLATFORM: ${{ matrix.platform }} 41 | 42 | package: 43 | name: "Build & verify package" 44 | runs-on: "ubuntu-latest" 45 | 46 | steps: 47 | - uses: "actions/checkout@v2" 48 | - uses: "actions/setup-python@v1" 49 | with: 50 | python-version: "3.8" 51 | 52 | - name: "Install pep517 and twine" 53 | run: "python -m pip install pep517 twine" 54 | - name: "Build package" 55 | run: "python -m pep517.build --source --binary ." 56 | - name: "List result" 57 | run: "ls -l dist" 58 | - name: "Check long_description" 59 | run: "python -m twine check dist/*" 60 | 61 | install-dev: 62 | strategy: 63 | matrix: 64 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 65 | 66 | name: "Verify dev env" 67 | runs-on: "${{ matrix.os }}" 68 | 69 | steps: 70 | - uses: "actions/checkout@v2" 71 | - uses: "actions/setup-python@v1" 72 | with: 73 | python-version: "3.8" 74 | - name: "Install in dev mode" 75 | run: "python -m pip install -e .[dev]" 76 | - name: "Import package" 77 | run: "python -c 'import rest_framework_yaml; print(rest_framework_yaml.__version__)'" 78 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.db 3 | *~ 4 | .* 5 | 6 | html/ 7 | htmlcov/ 8 | coverage/ 9 | build/ 10 | dist/ 11 | *.egg-info/ 12 | MANIFEST 13 | 14 | bin/ 15 | include/ 16 | lib/ 17 | local/ 18 | 19 | !.gitignore 20 | !.github 21 | !.flake8 22 | !.isort.cfg 23 | !.pre-commit-config.yaml 24 | 25 | pip-wheel-metadata 26 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 19.10b0 4 | hooks: 5 | - id: black 6 | language_version: python3.8 7 | 8 | - repo: https://gitlab.com/pycqa/flake8 9 | rev: 3.7.9 10 | hooks: 11 | - id: flake8 12 | language_version: python3.8 13 | 14 | - repo: https://github.com/asottile/seed-isort-config 15 | rev: v1.9.4 16 | hooks: 17 | - id: seed-isort-config 18 | 19 | - repo: https://github.com/pre-commit/mirrors-isort 20 | rev: v4.3.21 21 | hooks: 22 | - id: isort 23 | additional_dependencies: [toml] 24 | language_version: python3.8 25 | 26 | - repo: https://github.com/pre-commit/pre-commit-hooks 27 | rev: v2.4.0 28 | hooks: 29 | - id: trailing-whitespace 30 | - id: end-of-file-fixer 31 | - id: debug-statements 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: false 4 | 5 | env: 6 | - TOX_ENV=py27-flake8 7 | - TOX_ENV=py27-docs 8 | - TOX_ENV=py27-django1.6-drf2.4.3 9 | - TOX_ENV=py27-django1.6-drf2.4.4 10 | - TOX_ENV=py27-django1.6-drf3.0.0 11 | - TOX_ENV=py27-django1.7-drf2.4.3 12 | - TOX_ENV=py27-django1.7-drf2.4.4 13 | - TOX_ENV=py27-django1.7-drf3.0.0 14 | - TOX_ENV=py32-django1.6-drf2.4.3 15 | - TOX_ENV=py32-django1.6-drf2.4.4 16 | - TOX_ENV=py32-django1.6-drf3.0.0 17 | - TOX_ENV=py32-django1.7-drf2.4.3 18 | - TOX_ENV=py32-django1.7-drf2.4.4 19 | - TOX_ENV=py32-django1.7-drf3.0.0 20 | - TOX_ENV=py33-django1.6-drf2.4.3 21 | - TOX_ENV=py33-django1.6-drf2.4.4 22 | - TOX_ENV=py33-django1.6-drf3.0.0 23 | - TOX_ENV=py33-django1.7-drf2.4.3 24 | - TOX_ENV=py33-django1.7-drf2.4.4 25 | - TOX_ENV=py33-django1.7-drf3.0.0 26 | - TOX_ENV=py34-django1.6-drf2.4.3 27 | - TOX_ENV=py34-django1.6-drf2.4.4 28 | - TOX_ENV=py34-django1.6-drf3.0.0 29 | - TOX_ENV=py34-django1.7-drf2.4.3 30 | - TOX_ENV=py34-django1.7-drf2.4.4 31 | - TOX_ENV=py34-django1.7-drf3.0.0 32 | 33 | matrix: 34 | fast_finish: true 35 | 36 | install: 37 | - pip install tox 38 | 39 | script: 40 | - tox -e $TOX_ENV 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, José Padilla 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude * __pycache__ 2 | recursive-exclude * *.py[co] 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # REST Framework YAML 2 | 3 | [![build-status-image]][github-action] 4 | [![pypi-version]][pypi] 5 | 6 | **YAML support for Django REST Framework** 7 | 8 | Full documentation for the project is available at [http://jpadilla.github.io/django-rest-framework-yaml][docs]. 9 | 10 | ## Overview 11 | 12 | YAML support extracted as a third party package directly from the official Django REST Framework implementation. It's built using the [PyYAML][pyyaml] package. 13 | 14 | ## Requirements 15 | 16 | * Python (2.7, 3.3, 3.4) 17 | * Django (1.6, 1.7) 18 | 19 | ## Installation 20 | 21 | Install using `pip`... 22 | 23 | ```bash 24 | $ pip install djangorestframework-yaml 25 | ``` 26 | 27 | ## Example 28 | 29 | ```python 30 | REST_FRAMEWORK = { 31 | 'DEFAULT_PARSER_CLASSES': ( 32 | 'rest_framework_yaml.parsers.YAMLParser', 33 | ), 34 | 'DEFAULT_RENDERER_CLASSES': ( 35 | 'rest_framework_yaml.renderers.YAMLRenderer', 36 | ), 37 | } 38 | ``` 39 | 40 | You can also set the renderer and parser used for an individual view, or viewset, using the APIView class based views. 41 | 42 | ```python 43 | from rest_framework import routers, serializers, viewsets 44 | from rest_framework_yaml.parsers import YAMLParser 45 | from rest_framework_yaml.renderers import YAMLRenderer 46 | 47 | # Serializers define the API representation. 48 | class UserSerializer(serializers.HyperlinkedModelSerializer): 49 | class Meta: 50 | model = User 51 | fields = ('url', 'username', 'email', 'is_staff') 52 | 53 | 54 | # ViewSets define the view behavior. 55 | class UserViewSet(viewsets.ModelViewSet): 56 | queryset = User.objects.all() 57 | serializer_class = UserSerializer 58 | parser_classes = (YAMLParser,) 59 | renderer_classes = (YAMLRenderer,) 60 | ``` 61 | 62 | ### Sample output 63 | 64 | ```yaml 65 | --- 66 | - 67 | email: jpadilla@example.com 68 | is_staff: true 69 | url: "http://127.0.0.1:8000/users/1/" 70 | username: jpadilla 71 | ``` 72 | 73 | ## Documentation & Support 74 | 75 | Full documentation for the project is available at [http://jpadilla.github.io/django-rest-framework-yaml][docs]. 76 | 77 | You may also want to follow the [author][jpadilla] on Twitter. 78 | 79 | 80 | [build-status-image]: https://github.com/jpadilla/django-rest-framework-yaml/workflows/CI/badge.svg 81 | [github-action]: https://github.com/jpadilla/django-rest-framework-yaml/actions?query=workflow%3ACI 82 | [pypi-version]: https://img.shields.io/pypi/v/djangorestframework-yaml.svg 83 | [pypi]: https://pypi.python.org/pypi/djangorestframework-yaml 84 | [pyyaml]: http://pyyaml.org/ 85 | [docs]: http://jpadilla.github.io/django-rest-framework-yaml 86 | [jpadilla]: https://twitter.com/jpadilla_ 87 | -------------------------------------------------------------------------------- /docs/css/extra.css: -------------------------------------------------------------------------------- 1 | body.homepage div.col-md-9 h1:first-of-type { 2 | text-align: center; 3 | font-size: 60px; 4 | font-weight: 300; 5 | margin-top: 0; 6 | } 7 | 8 | body.homepage div.col-md-9 p:first-of-type { 9 | text-align: center; 10 | } 11 | 12 | body.homepage .badges { 13 | text-align: right; 14 | } 15 | 16 | body.homepage .badges a { 17 | display: inline-block; 18 | } 19 | 20 | body.homepage .badges a img { 21 | padding: 0; 22 | margin: 0; 23 | } 24 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | --- 11 | 12 | # REST Framework YAML 13 | 14 | YAML support for Django REST Framework 15 | 16 | --- 17 | 18 | ## Overview 19 | 20 | YAML support extracted as a third party package directly from the official Django REST Framework implementation. It's built using the [PyYAML][pyyaml] package. 21 | 22 | ## Requirements 23 | 24 | * Python (2.7, 3.3, 3.4) 25 | * Django (1.6, 1.7) 26 | 27 | ## Installation 28 | 29 | Install using `pip`... 30 | 31 | ```bash 32 | $ pip install djangorestframework-yaml 33 | ``` 34 | 35 | ## Example 36 | 37 | ```python 38 | REST_FRAMEWORK = { 39 | 'DEFAULT_PARSER_CLASSES': ( 40 | 'rest_framework_yaml.parsers.YAMLParser', 41 | ), 42 | 'DEFAULT_RENDERER_CLASSES': ( 43 | 'rest_framework_yaml.renderers.YAMLRenderer', 44 | ), 45 | } 46 | ``` 47 | 48 | You can also set the renderer and parser used for an individual view, or viewset, using the APIView class based views. 49 | 50 | ```python 51 | from rest_framework.response import Response 52 | from rest_framework.views import APIView 53 | from rest_framework_yaml.parsers import YAMLParser 54 | from rest_framework_yaml.renderers import YAMLRenderer 55 | 56 | class ExampleView(APIView): 57 | """ 58 | A view that can accept POST requests with YAML content. 59 | """ 60 | parser_classes = (YAMLParser,) 61 | renderer_classes = (YAMLRenderer,) 62 | 63 | def post(self, request, format=None): 64 | return Response({'received data': request.DATA}) 65 | ``` 66 | 67 | ### Sample output 68 | 69 | ```yaml 70 | --- 71 | - 72 | email: jpadilla@example.com 73 | is_staff: true 74 | url: "http://127.0.0.1:8000/users/1/" 75 | username: jpadilla 76 | ``` 77 | 78 | ## Testing 79 | 80 | Install testing requirements. 81 | 82 | ```bash 83 | $ pip install -r requirements.txt 84 | ``` 85 | 86 | Run with runtests. 87 | 88 | ```bash 89 | $ ./runtests.py 90 | ``` 91 | 92 | You can also use the excellent [tox](http://tox.readthedocs.org/en/latest/) testing tool to run the tests against all supported versions of Python and Django. Install tox globally, and then simply run: 93 | 94 | ```bash 95 | $ tox 96 | ``` 97 | 98 | ## Documentation 99 | 100 | To build the documentation, you'll need to install `mkdocs`. 101 | 102 | ```bash 103 | $ pip install mkdocs 104 | ``` 105 | 106 | To preview the documentation: 107 | 108 | ```bash 109 | $ mkdocs serve 110 | Running at: http://127.0.0.1:8000/ 111 | ``` 112 | 113 | To build the documentation: 114 | 115 | ```bash 116 | $ mkdocs build 117 | ``` 118 | 119 | 120 | [pyyaml]: http://pyyaml.org/ 121 | -------------------------------------------------------------------------------- /docs/parsers.md: -------------------------------------------------------------------------------- 1 | # Parsers 2 | 3 | ## Setting the parsers 4 | 5 | The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow requests with `YAML` content. 6 | 7 | REST_FRAMEWORK = { 8 | 'DEFAULT_PARSER_CLASSES': ( 9 | 'rest_framework_yaml.parsers.YAMLParser', 10 | ) 11 | } 12 | 13 | You can also set the parsers used for an individual view, or viewset, 14 | using the `APIView` class based views. 15 | 16 | from rest_framework.response import Response 17 | from rest_framework.views import APIView 18 | from rest_framework_yaml.parsers import YAMLParser 19 | 20 | class ExampleView(APIView): 21 | """ 22 | A view that can accept POST requests with YAML content. 23 | """ 24 | parser_classes = (YAMLParser,) 25 | 26 | def post(self, request, format=None): 27 | return Response({'received data': request.DATA}) 28 | 29 | Or, if you're using the `@api_view` decorator with function based views. 30 | 31 | @api_view(['POST']) 32 | @parser_classes((YAMLParser,)) 33 | def example_view(request, format=None): 34 | """ 35 | A view that can accept POST requests with YAML content. 36 | """ 37 | return Response({'received data': request.DATA}) 38 | 39 | --- 40 | 41 | # API Reference 42 | 43 | ## YAMLParser 44 | 45 | Parses `YAML` request content. 46 | 47 | Requires the `pyyaml` package to be installed. 48 | 49 | **.media_type**: `application/yaml` 50 | -------------------------------------------------------------------------------- /docs/renderers.md: -------------------------------------------------------------------------------- 1 | # Renderers 2 | 3 | ## Setting the renderers 4 | 5 | The default set of renderers may be set globally, using the `DEFAULT_RENDERER_CLASSES` setting. For example, the following settings would use `YAML` as the main media type and also include the self describing API. 6 | 7 | REST_FRAMEWORK = { 8 | 'DEFAULT_RENDERER_CLASSES': ( 9 | 'rest_framework_yaml.renderers.YAMLRenderer', 10 | ) 11 | } 12 | 13 | You can also set the renderers used for an individual view, or viewset, 14 | using the `APIView` class based views. 15 | 16 | from django.contrib.auth.models import User 17 | from rest_framework.response import Response 18 | from rest_framework.views import APIView 19 | from rest_framework_yaml.renderers import YAMLRenderer 20 | 21 | class UserCountView(APIView): 22 | """ 23 | A view that returns the count of active users in YAML. 24 | """ 25 | renderer_classes = (YAMLRenderer,) 26 | 27 | def get(self, request, format=None): 28 | user_count = User.objects.filter(active=True).count() 29 | content = {'user_count': user_count} 30 | return Response(content) 31 | 32 | Or, if you're using the `@api_view` decorator with function based views. 33 | 34 | @api_view(['GET']) 35 | @renderer_classes((YAMLRenderer,)) 36 | def user_count_view(request, format=None): 37 | """ 38 | A view that returns the count of active users in YAML. 39 | """ 40 | user_count = User.objects.filter(active=True).count() 41 | content = {'user_count': user_count} 42 | return Response(content) 43 | 44 | --- 45 | 46 | # API Reference 47 | 48 | ## YAMLRenderer 49 | 50 | Renders the request data into `YAML`. 51 | 52 | Requires the `pyyaml` package to be installed. 53 | 54 | Note that non-ascii characters will be rendered using `\uXXXX` character escape. For example: 55 | 56 | unicode black star: "\u2605" 57 | 58 | **.media_type**: `application/yaml` 59 | 60 | **.format**: `'.yaml'` 61 | 62 | **.charset**: `utf-8` 63 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: djangorestframework-yaml 2 | site_description: YAML support for Django REST Framework 3 | repo_url: https://github.com/jpadilla/django-rest-framework-yaml 4 | site_dir: html 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | 6 | [tool.black] 7 | line-length = 79 8 | 9 | 10 | [tool.isort] 11 | atomic=true 12 | force_grid_wrap=0 13 | include_trailing_comma=true 14 | lines_after_imports=2 15 | lines_between_types=1 16 | multi_line_output=3 17 | use_parentheses=true 18 | combine_as_imports=true 19 | 20 | known_first_party="rest_framework_yaml" 21 | known_third_party=["django", "pytest", "rest_framework", "setuptools"] 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Minimum Django and REST framework version 2 | Django>=1.6 3 | djangorestframework>=2.4.3 4 | 5 | # Test requirements 6 | pytest-django==2.6 7 | pytest==2.5.2 8 | pytest-cov==1.6 9 | flake8==2.2.2 10 | 11 | # wheel for PyPI installs 12 | wheel==0.24.0 13 | 14 | # MkDocs for documentation previews/deploys 15 | mkdocs==0.11.1 16 | -------------------------------------------------------------------------------- /rest_framework_yaml/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.0.0" 2 | -------------------------------------------------------------------------------- /rest_framework_yaml/compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | The `compat` module provides support for backwards compatibility with older 3 | versions of django/python, and compatibility wrappers around optional packages. 4 | """ 5 | # flake8: noqa 6 | 7 | try: 8 | import yaml 9 | except ImportError: 10 | yaml = represent_text = None 11 | else: 12 | yaml_represent_text = yaml.representer.SafeRepresenter.represent_str 13 | 14 | # OrderedDict only available in Python 2.7. 15 | # This will always be the case in Django 1.7 and above, as these versions 16 | # no longer support Python 2.6. 17 | # For Django <= 1.6 and Python 2.6 fall back to SortedDict. 18 | try: 19 | from collections import OrderedDict 20 | except: 21 | from django.utils.datastructures import SortedDict as OrderedDict 22 | 23 | try: 24 | # Note: Hyperlink is private(?) API from DRF 3 25 | from rest_framework.relations import Hyperlink 26 | except ImportError: 27 | Hyperlink = None 28 | 29 | try: 30 | # Note: ReturnDict and ReturnList are private API from DRF 3 31 | from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList 32 | except ImportError: 33 | ReturnDict = None 34 | ReturnList = None 35 | -------------------------------------------------------------------------------- /rest_framework_yaml/encoders.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper classes for parsers. 3 | """ 4 | from __future__ import unicode_literals 5 | 6 | import decimal 7 | import types 8 | 9 | from django.utils.encoding import force_str 10 | 11 | from .compat import ( 12 | Hyperlink, 13 | OrderedDict, 14 | ReturnDict, 15 | ReturnList, 16 | yaml, 17 | yaml_represent_text, 18 | ) 19 | 20 | 21 | class SafeDumper(yaml.SafeDumper): 22 | """ 23 | Handles decimals as strings. 24 | Handles OrderedDicts as usual dicts, but preserves field order, rather 25 | than the usual behaviour of sorting the keys. 26 | 27 | Adapted from http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py 28 | """ 29 | 30 | def represent_decimal(self, data): 31 | return self.represent_scalar("tag:yaml.org,2002:str", force_str(data)) 32 | 33 | def represent_mapping(self, tag, mapping, flow_style=None): 34 | value = [] 35 | node = yaml.MappingNode(tag, value, flow_style=flow_style) 36 | if self.alias_key is not None: 37 | self.represented_objects[self.alias_key] = node 38 | best_style = True 39 | if hasattr(mapping, "items"): 40 | mapping = list(mapping.items()) 41 | if not isinstance(mapping, OrderedDict): 42 | mapping.sort() 43 | for item_key, item_value in mapping: 44 | node_key = self.represent_data(item_key) 45 | node_value = self.represent_data(item_value) 46 | if not ( 47 | isinstance(node_key, yaml.ScalarNode) and not node_key.style 48 | ): 49 | best_style = False 50 | if not ( 51 | isinstance(node_value, yaml.ScalarNode) 52 | and not node_value.style 53 | ): 54 | best_style = False 55 | value.append((node_key, node_value)) 56 | if flow_style is None: 57 | if self.default_flow_style is not None: 58 | node.flow_style = self.default_flow_style 59 | else: 60 | node.flow_style = best_style 61 | return node 62 | 63 | 64 | SafeDumper.add_representer(decimal.Decimal, SafeDumper.represent_decimal) 65 | 66 | SafeDumper.add_representer( 67 | OrderedDict, yaml.representer.SafeRepresenter.represent_dict 68 | ) 69 | 70 | SafeDumper.add_representer( 71 | types.GeneratorType, yaml.representer.SafeRepresenter.represent_list 72 | ) 73 | 74 | if Hyperlink: 75 | SafeDumper.add_representer(Hyperlink, yaml_represent_text) 76 | 77 | if ReturnDict: 78 | SafeDumper.add_representer( 79 | ReturnDict, yaml.representer.SafeRepresenter.represent_dict 80 | ) 81 | 82 | if ReturnList: 83 | SafeDumper.add_representer( 84 | ReturnList, yaml.representer.SafeRepresenter.represent_list 85 | ) 86 | -------------------------------------------------------------------------------- /rest_framework_yaml/parsers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides YAML parsing support. 3 | """ 4 | from __future__ import unicode_literals 5 | 6 | from django.conf import settings 7 | from django.utils.encoding import force_str 8 | from rest_framework.exceptions import ParseError 9 | from rest_framework.parsers import BaseParser 10 | 11 | from .compat import yaml 12 | 13 | 14 | class YAMLParser(BaseParser): 15 | """ 16 | Parses YAML-serialized data. 17 | """ 18 | 19 | media_type = "application/yaml" 20 | 21 | def parse(self, stream, media_type=None, parser_context=None): 22 | """ 23 | Parses the incoming bytestream as YAML and returns the resulting data. 24 | """ 25 | assert yaml, "YAMLParser requires pyyaml to be installed" 26 | 27 | parser_context = parser_context or {} 28 | encoding = parser_context.get("encoding", settings.DEFAULT_CHARSET) 29 | 30 | try: 31 | data = stream.read().decode(encoding) 32 | return yaml.safe_load(data) 33 | except (ValueError, yaml.parser.ParserError) as exc: 34 | raise ParseError("YAML parse error - %s" % force_str(exc)) 35 | -------------------------------------------------------------------------------- /rest_framework_yaml/renderers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides YAML rendering support. 3 | """ 4 | from __future__ import unicode_literals 5 | 6 | from rest_framework.renderers import BaseRenderer 7 | 8 | from .compat import yaml 9 | from .encoders import SafeDumper 10 | 11 | 12 | class YAMLRenderer(BaseRenderer): 13 | """ 14 | Renderer which serializes to YAML. 15 | """ 16 | 17 | media_type = "application/yaml" 18 | format = "yaml" 19 | encoder = SafeDumper 20 | charset = "utf-8" 21 | ensure_ascii = False 22 | default_flow_style = False 23 | 24 | def render(self, data, accepted_media_type=None, renderer_context=None): 25 | """ 26 | Renders `data` into serialized YAML. 27 | """ 28 | assert yaml, "YAMLRenderer requires pyyaml to be installed" 29 | 30 | if data is None: 31 | return "" 32 | 33 | return yaml.dump( 34 | data, 35 | stream=None, 36 | encoding=self.charset, 37 | Dumper=self.encoder, 38 | allow_unicode=not self.ensure_ascii, 39 | default_flow_style=self.default_flow_style, 40 | ) 41 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | from __future__ import print_function 3 | 4 | import os 5 | import subprocess 6 | import sys 7 | 8 | import pytest 9 | 10 | 11 | PYTEST_ARGS = { 12 | "default": ["tests"], 13 | "fast": ["tests", "-q"], 14 | } 15 | 16 | FLAKE8_ARGS = ["rest_framework_yaml", "tests", "--ignore=E501"] 17 | 18 | 19 | sys.path.append(os.path.dirname(__file__)) 20 | 21 | 22 | def exit_on_failure(ret, message=None): 23 | if ret: 24 | sys.exit(ret) 25 | 26 | 27 | def flake8_main(args): 28 | print("Running flake8 code linting") 29 | ret = subprocess.call(["flake8"] + args) 30 | print("flake8 failed" if ret else "flake8 passed") 31 | return ret 32 | 33 | 34 | def split_class_and_function(string): 35 | class_string, function_string = string.split(".", 1) 36 | return "%s and %s" % (class_string, function_string) 37 | 38 | 39 | def is_function(string): 40 | # `True` if it looks like a test function is included in the string. 41 | return string.startswith("test_") or ".test_" in string 42 | 43 | 44 | def is_class(string): 45 | # `True` if first character is uppercase - assume it's a class name. 46 | return string[0] == string[0].upper() 47 | 48 | 49 | if __name__ == "__main__": 50 | try: 51 | sys.argv.remove("--nolint") 52 | except ValueError: 53 | run_flake8 = True 54 | else: 55 | run_flake8 = False 56 | 57 | try: 58 | sys.argv.remove("--lintonly") 59 | except ValueError: 60 | run_tests = True 61 | else: 62 | run_tests = False 63 | 64 | try: 65 | sys.argv.remove("--fast") 66 | except ValueError: 67 | style = "default" 68 | else: 69 | style = "fast" 70 | run_flake8 = False 71 | 72 | if len(sys.argv) > 1: 73 | pytest_args = sys.argv[1:] 74 | first_arg = pytest_args[0] 75 | if first_arg.startswith("-"): 76 | # `runtests.py [flags]` 77 | pytest_args = ["tests"] + pytest_args 78 | elif is_class(first_arg) and is_function(first_arg): 79 | # `runtests.py TestCase.test_function [flags]` 80 | expression = split_class_and_function(first_arg) 81 | pytest_args = ["tests", "-k", expression] + pytest_args[1:] 82 | elif is_class(first_arg) or is_function(first_arg): 83 | # `runtests.py TestCase [flags]` 84 | # `runtests.py test_function [flags]` 85 | pytest_args = ["tests", "-k", pytest_args[0]] + pytest_args[1:] 86 | else: 87 | pytest_args = PYTEST_ARGS[style] 88 | 89 | if run_tests: 90 | exit_on_failure(pytest.main(pytest_args)) 91 | if run_flake8: 92 | exit_on_failure(flake8_main(FLAKE8_ARGS)) 93 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | import sys 6 | 7 | from setuptools import setup 8 | from setuptools.command.test import test as TestCommand 9 | 10 | 11 | # This command has been borrowed from 12 | # https://github.com/getsentry/sentry/blob/master/setup.py 13 | class PyTest(TestCommand): 14 | def finalize_options(self): 15 | TestCommand.finalize_options(self) 16 | self.test_args = ["tests"] 17 | self.test_suite = True 18 | 19 | def run_tests(self): 20 | import pytest 21 | 22 | errno = pytest.main(self.test_args) 23 | sys.exit(errno) 24 | 25 | 26 | def read(f): 27 | return open(f, "r", encoding="utf-8").read() 28 | 29 | 30 | def get_version(package): 31 | """ 32 | Return package version as listed in `__version__` in `init.py`. 33 | """ 34 | init_py = open(os.path.join(package, "__init__.py")).read() 35 | return re.search( 36 | "^__version__ = ['\"]([^'\"]+)['\"]", init_py, re.MULTILINE 37 | ).group(1) 38 | 39 | 40 | def get_packages(package): 41 | """ 42 | Return root package and all sub-packages. 43 | """ 44 | return [ 45 | dirpath 46 | for dirpath, dirnames, filenames in os.walk(package) 47 | if os.path.exists(os.path.join(dirpath, "__init__.py")) 48 | ] 49 | 50 | 51 | def get_package_data(package): 52 | """ 53 | Return all files under the root package, that are not in a 54 | package themselves. 55 | """ 56 | walk = [ 57 | (dirpath.replace(package + os.sep, "", 1), filenames) 58 | for dirpath, dirnames, filenames in os.walk(package) 59 | if not os.path.exists(os.path.join(dirpath, "__init__.py")) 60 | ] 61 | 62 | filepaths = [] 63 | for base, filenames in walk: 64 | filepaths.extend( 65 | [os.path.join(base, filename) for filename in filenames] 66 | ) 67 | return {package: filepaths} 68 | 69 | 70 | name = "djangorestframework-yaml" 71 | package = "rest_framework_yaml" 72 | version = get_version(package) 73 | description = "YAML support for Django REST Framework" 74 | url = "https://github.com/jpadilla/django-rest-framework-yaml" 75 | author = "José Padilla" 76 | author_email = "hello@jpadilla.com" 77 | license = "BSD" 78 | install_requires = [ 79 | "PyYAML>=3.10", 80 | ] 81 | extras_requires = { 82 | "docs": ["mkdocs>=0.11.1"], 83 | "tests": [ 84 | "Django>=1.6", 85 | "djangorestframework>=2.4.3", 86 | "pytest-django", 87 | "pytest", 88 | "flake8", 89 | ], 90 | } 91 | 92 | extras_requires["dev"] = ( 93 | extras_requires["docs"] + extras_requires["tests"] + ["tox", "pre-commit"] 94 | ) 95 | 96 | 97 | if sys.argv[-1] == "publish": 98 | os.system("python setup.py sdist upload") 99 | os.system("python setup.py bdist_wheel upload") 100 | print("You probably want to also tag the version now:") 101 | print(" git tag -a {0} -m 'version {0}'".format(version)) 102 | print(" git push --tags") 103 | sys.exit() 104 | 105 | setup( 106 | name=name, 107 | version=version, 108 | url=url, 109 | license=license, 110 | description=description, 111 | long_description=read("README.md"), 112 | long_description_content_type="text/markdown", 113 | author=author, 114 | author_email=author_email, 115 | packages=get_packages(package), 116 | package_data=get_package_data(package), 117 | cmdclass={"test": PyTest}, 118 | install_requires=install_requires, 119 | extras_require=extras_requires, 120 | python_requires=">=3.5", 121 | classifiers=[ 122 | "Development Status :: 5 - Production/Stable", 123 | "Environment :: Web Environment", 124 | "Framework :: Django", 125 | "Intended Audience :: Developers", 126 | "License :: OSI Approved :: BSD License", 127 | "Operating System :: OS Independent", 128 | "Natural Language :: English", 129 | "Programming Language :: Python :: 3", 130 | "Programming Language :: Python :: 3.5", 131 | "Programming Language :: Python :: 3.6", 132 | "Programming Language :: Python :: 3.7", 133 | "Programming Language :: Python :: 3.8", 134 | "Topic :: Internet :: WWW/HTTP", 135 | ], 136 | ) 137 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpadilla/django-rest-framework-yaml/2e0e219e489984f82c0bad458e0b7174aec110c9/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | def pytest_configure(): 2 | from django.conf import settings 3 | 4 | settings.configure( 5 | DEBUG_PROPAGATE_EXCEPTIONS=True, 6 | DATABASES={ 7 | "default": { 8 | "ENGINE": "django.db.backends.sqlite3", 9 | "NAME": ":memory:", 10 | } 11 | }, 12 | SITE_ID=1, 13 | SECRET_KEY="not very secret in tests", 14 | USE_I18N=True, 15 | USE_L10N=True, 16 | STATIC_URL="/static/", 17 | ROOT_URLCONF="tests.urls", 18 | TEMPLATE_LOADERS=( 19 | "django.template.loaders.filesystem.Loader", 20 | "django.template.loaders.app_directories.Loader", 21 | ), 22 | MIDDLEWARE_CLASSES=( 23 | "django.middleware.common.CommonMiddleware", 24 | "django.contrib.sessions.middleware.SessionMiddleware", 25 | "django.middleware.csrf.CsrfViewMiddleware", 26 | "django.contrib.auth.middleware.AuthenticationMiddleware", 27 | "django.contrib.messages.middleware.MessageMiddleware", 28 | ), 29 | INSTALLED_APPS=( 30 | "django.contrib.auth", 31 | "django.contrib.contenttypes", 32 | "django.contrib.sessions", 33 | "django.contrib.sites", 34 | "django.contrib.messages", 35 | "django.contrib.staticfiles", 36 | "rest_framework", 37 | "rest_framework.authtoken", 38 | "tests", 39 | ), 40 | PASSWORD_HASHERS=( 41 | "django.contrib.auth.hashers.SHA1PasswordHasher", 42 | "django.contrib.auth.hashers.PBKDF2PasswordHasher", 43 | "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", 44 | "django.contrib.auth.hashers.BCryptPasswordHasher", 45 | "django.contrib.auth.hashers.MD5PasswordHasher", 46 | "django.contrib.auth.hashers.CryptPasswordHasher", 47 | ), 48 | ) 49 | 50 | try: 51 | import django 52 | 53 | django.setup() 54 | except AttributeError: 55 | pass 56 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpadilla/django-rest-framework-yaml/2e0e219e489984f82c0bad458e0b7174aec110c9/tests/models.py -------------------------------------------------------------------------------- /tests/test_renderers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import unittest 5 | 6 | from decimal import Decimal 7 | from io import BytesIO 8 | 9 | from django.test import TestCase 10 | 11 | from rest_framework_yaml.compat import Hyperlink 12 | from rest_framework_yaml.parsers import YAMLParser 13 | from rest_framework_yaml.renderers import YAMLRenderer 14 | 15 | 16 | class YAMLRendererTests(TestCase): 17 | """ 18 | Tests specific to the YAML Renderer 19 | """ 20 | 21 | def test_render(self): 22 | """ 23 | Test basic YAML rendering. 24 | """ 25 | _yaml_repr = "foo:\n- bar\n- baz\n" 26 | 27 | obj = {"foo": ["bar", "baz"]} 28 | 29 | renderer = YAMLRenderer() 30 | content = renderer.render(obj, "application/yaml") 31 | 32 | self.assertEqual(content.decode("utf-8"), _yaml_repr) 33 | 34 | def test_render_and_parse(self): 35 | """ 36 | Test rendering and then parsing returns the original object. 37 | IE obj -> render -> parse -> obj. 38 | """ 39 | obj = {"foo": ["bar", "baz"]} 40 | 41 | renderer = YAMLRenderer() 42 | parser = YAMLParser() 43 | 44 | content = renderer.render(obj, "application/yaml") 45 | data = parser.parse(BytesIO(content)) 46 | self.assertEqual(obj, data) 47 | 48 | def test_render_decimal(self): 49 | """ 50 | Test YAML decimal rendering. 51 | """ 52 | renderer = YAMLRenderer() 53 | content = renderer.render( 54 | {"field": Decimal("111.2")}, "application/yaml" 55 | ) 56 | self.assertYAMLContains(content.decode("utf-8"), "field: '111.2'") 57 | 58 | @unittest.skipUnless(Hyperlink, "Hyperlink is undefined") 59 | def test_render_hyperlink(self): 60 | """ 61 | Test YAML Hyperlink rendering. 62 | """ 63 | renderer = YAMLRenderer() 64 | content = renderer.render( 65 | {"field": Hyperlink("http://pépé.com?great-answer=42", "test")}, 66 | "application/yaml", 67 | ) 68 | self.assertYAMLContains( 69 | content.decode("utf-8"), "field: http://pépé.com?great-answer=42" 70 | ) 71 | 72 | def assertYAMLContains(self, content, string): 73 | self.assertTrue(string in content, "%r not in %r" % (string, content)) 74 | 75 | def test_proper_encoding(self): 76 | _yaml_repr = "countries:\n- United Kingdom\n- France\n- España" 77 | obj = {"countries": ["United Kingdom", "France", "España"]} 78 | renderer = YAMLRenderer() 79 | content = renderer.render(obj, "application/yaml") 80 | self.assertEqual(content.strip(), _yaml_repr.encode("utf-8")) 81 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | strict = true 3 | addopts = -ra 4 | testpaths = tests 5 | filterwarnings = 6 | once::Warning 7 | ignore:::pympler[.*] 8 | 9 | 10 | [gh-actions] 11 | python = 12 | 3.5: py35 13 | 3.6: py36 14 | 3.7: py37 15 | 3.8: py38, lint, docs 16 | 17 | 18 | [tox] 19 | envlist = 20 | lint 21 | {py35,py36,py37}-django2.2-drf3.11 22 | {py36,py37,py38}-django3.0-drf3.11 23 | docs 24 | isolated_build = True 25 | 26 | 27 | [testenv] 28 | commands = pytest {posargs} 29 | deps = 30 | django2.2: Django==2.2.* 31 | django3.0: Django==3.0.* 32 | drf3.11: djangorestframework==3.11.* 33 | pytest-django 34 | 35 | 36 | [testenv:lint] 37 | basepython = python3.8 38 | extras = dev 39 | passenv = HOMEPATH # needed on Windows 40 | commands = pre-commit run --all-files 41 | 42 | 43 | [testenv:docs] 44 | basepython = python3.8 45 | extras = docs 46 | commands = mkdocs build 47 | --------------------------------------------------------------------------------