django-headless 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Leon van der Grient
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-headless
3
+ Version: 1.0.0
4
+ Summary: Automagically create a REST API for your Django models
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: django,rest,api,headless,cms
8
+ Author: Leon van der Grient
9
+ Author-email: leon@dwarsbit.nl
10
+ Requires-Python: >=3.12
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Framework :: Django
18
+ Classifier: Framework :: Django :: 5.2
19
+ Classifier: Framework :: Django :: 6.0
20
+ Classifier: Framework :: Django :: 6.1
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Dist: Django (>=5.2,<7.0)
24
+ Requires-Dist: djangorestframework (>=3.18,<4.0)
25
+ Requires-Dist: drf-flex-fields (>=1.0,<2.0)
26
+ Requires-Dist: rich (>=14)
27
+ Project-URL: Changelog, https://github.com/dwarsbit/django-headless/blob/main/CHANGELOG.md
28
+ Project-URL: Documentation, https://djangoheadless.org/docs/intro
29
+ Project-URL: Homepage, https://djangoheadless.org
30
+ Project-URL: Issues, https://github.com/dwarsbit/django-headless/issues
31
+ Project-URL: Source, https://github.com/dwarsbit/django-headless
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Django Headless
35
+
36
+ [![PyPI version](https://badge.fury.io/py/django-headless.svg)](https://badge.fury.io/py/django-headless)
37
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-headless.svg)](https://pypi.org/project/django-headless/)
38
+ [![Django versions](https://img.shields.io/badge/django-5.2%2B-blue.svg)](https://www.djangoproject.com/)
39
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
40
+
41
+ Django Headless generates a REST API for your Django models, so Django can serve as the backend of a headless CMS. Decorate a model with `@expose()` and it gets list, detail, create, update and delete routes, with no serializers, viewsets or URL patterns to write.
42
+
43
+ Read the documentation at [djangoheadless.org](https://djangoheadless.org).
44
+
45
+ ## Features
46
+
47
+ - **Routes per model**: one decorator generates the routes of a model.
48
+ - **Singletons**: site settings and other one-off objects as a single route.
49
+ - **Filtering**: every lookup a field supports, from the query string.
50
+ - **Lookup routes**: fetch an object by a unique field such as a slug, at `/api/blog.article/by_slug/<value>`.
51
+ - **Pagination**: a stable, paginated list envelope with ready-to-fetch links.
52
+ - **Protected by default**: generated routes only answer staff users until you open them up.
53
+
54
+ ## Quick start
55
+
56
+ 1. Install the package:
57
+
58
+ ```bash
59
+ pip install django-headless
60
+ ```
61
+
62
+ 2. Add `rest_framework`, `headless` and `headless.rest` to `INSTALLED_APPS`:
63
+
64
+ ```python
65
+ # mysite/settings.py
66
+ INSTALLED_APPS = [
67
+ # ...
68
+ "rest_framework",
69
+ "headless",
70
+ "headless.rest",
71
+ ]
72
+ ```
73
+
74
+ 3. Include the API in your URL configuration:
75
+
76
+ ```python
77
+ # mysite/urls.py
78
+ from django.urls import include, path
79
+
80
+ urlpatterns = [
81
+ # ...
82
+ path("api/", include("headless.rest.urls")),
83
+ ]
84
+ ```
85
+
86
+ 4. Expose a model:
87
+
88
+ ```python
89
+ # blog/models.py
90
+ from django.db import models
91
+ from headless import expose
92
+
93
+
94
+ @expose()
95
+ class Article(models.Model):
96
+ title = models.CharField(max_length=200)
97
+ body = models.TextField()
98
+ published = models.BooleanField(default=False)
99
+ created_at = models.DateTimeField(auto_now_add=True)
100
+
101
+ def __str__(self):
102
+ return self.title
103
+ ```
104
+
105
+ Run `python manage.py runserver`; the article list is at `/api/blog.article`.
106
+
107
+ Generated routes answer `403 Forbidden` to everyone but staff users, so an exposed model is never public by accident. To configure authentication or open the routes up, see [Permissions](https://djangoheadless.org/docs/permissions).
108
+
109
+ ## Documentation
110
+
111
+ - [Installation](https://djangoheadless.org/docs/installation): requirements and setup
112
+ - [Exposing models](https://djangoheadless.org/docs/models): the `@expose()` options
113
+ - [Singletons](https://djangoheadless.org/docs/singletons): single-instance resources such as site settings
114
+ - [Filtering](https://djangoheadless.org/docs/filtering): lookups, exclusions and type casting
115
+ - [Pagination](https://djangoheadless.org/docs/pagination): the list envelope
116
+ - [Authentication](https://djangoheadless.org/docs/authentication): secret key and DRF authentication
117
+ - [Serializers](https://djangoheadless.org/docs/serializers): expanding relations and model properties
118
+ - [Permissions](https://djangoheadless.org/docs/permissions): defaults and overrides for generated routes
119
+ - [Settings reference](https://djangoheadless.org/docs/settings): every `HEADLESS` setting
120
+ - [System checks](https://djangoheadless.org/docs/checks): the checks for risky or broken settings
121
+
122
+ ## Contributing
123
+
124
+ To contribute a change:
125
+
126
+ 1. Fork the repository.
127
+ 2. Create a branch: `git checkout -b fix-boolean-casting`.
128
+ 3. Make the change and add tests for it.
129
+ 4. Run the test suite: `poetry run python scripts/test_wrapper.py test`.
130
+ 5. Commit and push the branch.
131
+ 6. Open a pull request.
132
+
133
+ Set up a development environment with Poetry:
134
+
135
+ ```bash
136
+ git clone https://github.com/dwarsbit/django-headless.git
137
+ cd django-headless
138
+ poetry install
139
+ poetry run python scripts/test_wrapper.py test
140
+ ```
141
+
142
+ Writing for the README or the documentation follows [STYLE.md](STYLE.md).
143
+
144
+ ## Support
145
+
146
+ - Bug reports: [GitHub Issues](https://github.com/dwarsbit/django-headless/issues)
147
+ - Questions: [GitHub Discussions](https://github.com/dwarsbit/django-headless/discussions)
148
+ - Email: leon@dwarsbit.nl
149
+
150
+ ## License
151
+
152
+ Django Headless is licensed under the MIT License; see [LICENSE](LICENSE).
153
+
154
+ ## Links
155
+
156
+ - [Documentation](https://djangoheadless.org)
157
+ - [PyPI](https://pypi.org/project/django-headless/)
158
+ - [GitHub](https://github.com/dwarsbit/django-headless)
159
+ - [Changelog](CHANGELOG.md)
160
+
@@ -0,0 +1,126 @@
1
+ # Django Headless
2
+
3
+ [![PyPI version](https://badge.fury.io/py/django-headless.svg)](https://badge.fury.io/py/django-headless)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/django-headless.svg)](https://pypi.org/project/django-headless/)
5
+ [![Django versions](https://img.shields.io/badge/django-5.2%2B-blue.svg)](https://www.djangoproject.com/)
6
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
7
+
8
+ Django Headless generates a REST API for your Django models, so Django can serve as the backend of a headless CMS. Decorate a model with `@expose()` and it gets list, detail, create, update and delete routes, with no serializers, viewsets or URL patterns to write.
9
+
10
+ Read the documentation at [djangoheadless.org](https://djangoheadless.org).
11
+
12
+ ## Features
13
+
14
+ - **Routes per model**: one decorator generates the routes of a model.
15
+ - **Singletons**: site settings and other one-off objects as a single route.
16
+ - **Filtering**: every lookup a field supports, from the query string.
17
+ - **Lookup routes**: fetch an object by a unique field such as a slug, at `/api/blog.article/by_slug/<value>`.
18
+ - **Pagination**: a stable, paginated list envelope with ready-to-fetch links.
19
+ - **Protected by default**: generated routes only answer staff users until you open them up.
20
+
21
+ ## Quick start
22
+
23
+ 1. Install the package:
24
+
25
+ ```bash
26
+ pip install django-headless
27
+ ```
28
+
29
+ 2. Add `rest_framework`, `headless` and `headless.rest` to `INSTALLED_APPS`:
30
+
31
+ ```python
32
+ # mysite/settings.py
33
+ INSTALLED_APPS = [
34
+ # ...
35
+ "rest_framework",
36
+ "headless",
37
+ "headless.rest",
38
+ ]
39
+ ```
40
+
41
+ 3. Include the API in your URL configuration:
42
+
43
+ ```python
44
+ # mysite/urls.py
45
+ from django.urls import include, path
46
+
47
+ urlpatterns = [
48
+ # ...
49
+ path("api/", include("headless.rest.urls")),
50
+ ]
51
+ ```
52
+
53
+ 4. Expose a model:
54
+
55
+ ```python
56
+ # blog/models.py
57
+ from django.db import models
58
+ from headless import expose
59
+
60
+
61
+ @expose()
62
+ class Article(models.Model):
63
+ title = models.CharField(max_length=200)
64
+ body = models.TextField()
65
+ published = models.BooleanField(default=False)
66
+ created_at = models.DateTimeField(auto_now_add=True)
67
+
68
+ def __str__(self):
69
+ return self.title
70
+ ```
71
+
72
+ Run `python manage.py runserver`; the article list is at `/api/blog.article`.
73
+
74
+ Generated routes answer `403 Forbidden` to everyone but staff users, so an exposed model is never public by accident. To configure authentication or open the routes up, see [Permissions](https://djangoheadless.org/docs/permissions).
75
+
76
+ ## Documentation
77
+
78
+ - [Installation](https://djangoheadless.org/docs/installation): requirements and setup
79
+ - [Exposing models](https://djangoheadless.org/docs/models): the `@expose()` options
80
+ - [Singletons](https://djangoheadless.org/docs/singletons): single-instance resources such as site settings
81
+ - [Filtering](https://djangoheadless.org/docs/filtering): lookups, exclusions and type casting
82
+ - [Pagination](https://djangoheadless.org/docs/pagination): the list envelope
83
+ - [Authentication](https://djangoheadless.org/docs/authentication): secret key and DRF authentication
84
+ - [Serializers](https://djangoheadless.org/docs/serializers): expanding relations and model properties
85
+ - [Permissions](https://djangoheadless.org/docs/permissions): defaults and overrides for generated routes
86
+ - [Settings reference](https://djangoheadless.org/docs/settings): every `HEADLESS` setting
87
+ - [System checks](https://djangoheadless.org/docs/checks): the checks for risky or broken settings
88
+
89
+ ## Contributing
90
+
91
+ To contribute a change:
92
+
93
+ 1. Fork the repository.
94
+ 2. Create a branch: `git checkout -b fix-boolean-casting`.
95
+ 3. Make the change and add tests for it.
96
+ 4. Run the test suite: `poetry run python scripts/test_wrapper.py test`.
97
+ 5. Commit and push the branch.
98
+ 6. Open a pull request.
99
+
100
+ Set up a development environment with Poetry:
101
+
102
+ ```bash
103
+ git clone https://github.com/dwarsbit/django-headless.git
104
+ cd django-headless
105
+ poetry install
106
+ poetry run python scripts/test_wrapper.py test
107
+ ```
108
+
109
+ Writing for the README or the documentation follows [STYLE.md](STYLE.md).
110
+
111
+ ## Support
112
+
113
+ - Bug reports: [GitHub Issues](https://github.com/dwarsbit/django-headless/issues)
114
+ - Questions: [GitHub Discussions](https://github.com/dwarsbit/django-headless/discussions)
115
+ - Email: leon@dwarsbit.nl
116
+
117
+ ## License
118
+
119
+ Django Headless is licensed under the MIT License; see [LICENSE](LICENSE).
120
+
121
+ ## Links
122
+
123
+ - [Documentation](https://djangoheadless.org)
124
+ - [PyPI](https://pypi.org/project/django-headless/)
125
+ - [GitHub](https://github.com/dwarsbit/django-headless)
126
+ - [Changelog](CHANGELOG.md)
@@ -0,0 +1,83 @@
1
+ __title__ = "Django Headless"
2
+ __version__ = "1.0.0"
3
+ __author__ = "Leon van der Grient"
4
+ __license__ = "MIT"
5
+
6
+ from typing import Type
7
+
8
+ from django.db import models
9
+
10
+ from .registry import headless_registry
11
+
12
+ # Version synonym
13
+ VERSION = __version__
14
+
15
+
16
+ def expose(singleton=False, search_fields=None, fields=None, exclude=None, read_only=False, lookup_fields=None):
17
+ """
18
+ Decorator to register a Django model to the headless registry.
19
+
20
+ Args:
21
+ singleton: If True, the model will be treated as a singleton (single instance).
22
+ search_fields: List of field names to enable search functionality on.
23
+ fields: Restrict the exposed serializer to these field names.
24
+ exclude: Exclude these field names from the exposed serializer.
25
+ read_only: If True, the model is exposed read-only (no create, update or delete).
26
+ lookup_fields: Names of unique fields to expose a retrieve route for, at by_<field>/<value>.
27
+
28
+ Usage:
29
+ @expose()
30
+ class MyModel(models.Model):
31
+ pass
32
+ """
33
+
34
+ def decorator(model_class: Type[models.Model]):
35
+ expose_model(
36
+ model_class,
37
+ singleton=singleton,
38
+ search_fields=search_fields,
39
+ fields=fields,
40
+ exclude=exclude,
41
+ read_only=read_only,
42
+ lookup_fields=lookup_fields,
43
+ )
44
+
45
+ return model_class
46
+
47
+ return decorator
48
+
49
+
50
+ def expose_model(
51
+ model_class: Type[models.Model],
52
+ singleton=False,
53
+ search_fields=None,
54
+ fields=None,
55
+ exclude=None,
56
+ read_only=False,
57
+ lookup_fields=None,
58
+ ):
59
+ """
60
+ Register a Django model to the headless registry.
61
+
62
+ Args:
63
+ model_class: The Django model class to expose via the REST API.
64
+ singleton: If True, the model will be treated as a singleton (single instance).
65
+ search_fields: List of field names to enable search functionality on.
66
+ fields: Restrict the exposed serializer to these field names.
67
+ exclude: Exclude these field names from the exposed serializer.
68
+ read_only: If True, the model is exposed read-only (no create, update or delete).
69
+ lookup_fields: Names of unique fields to expose a retrieve route for, at by_<field>/<value>.
70
+
71
+ Usage:
72
+ expose_model(MyModel, singleton=False, search_fields=['name', 'description'])
73
+ """
74
+
75
+ headless_registry.register(
76
+ model_class,
77
+ singleton=singleton,
78
+ search_fields=search_fields,
79
+ fields=fields,
80
+ exclude=exclude,
81
+ read_only=read_only,
82
+ lookup_fields=lookup_fields,
83
+ )
@@ -0,0 +1,49 @@
1
+ from django.apps import AppConfig
2
+
3
+ from . import VERSION
4
+ from .utils import (
5
+ log,
6
+ is_boot_log_enabled,
7
+ get_latest_version,
8
+ is_newer_version,
9
+ normalize_version,
10
+ )
11
+
12
+
13
+ class DjangoHeadlessConfig(AppConfig):
14
+ name = "headless"
15
+ label = "headless"
16
+
17
+ def ready(self):
18
+ # Register the system checks of the registry and settings. The headless
19
+ # app manages the registry; apps such as headless.rest serve the models.
20
+ from . import checks # noqa: F401
21
+
22
+ if is_boot_log_enabled():
23
+ self.log_boot()
24
+
25
+ def log_boot(self):
26
+ """
27
+ Log the version and the exposed models. Exposure apps such as
28
+ headless.rest log their own part of the boot log after this.
29
+ """
30
+ from headless.settings import headless_settings
31
+ from .registry import headless_registry
32
+
33
+ log("")
34
+ log("[bold magenta]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold magenta]")
35
+ log("[bold cyan]Django Headless[/bold cyan]")
36
+
37
+ # Check for a newer version, unless the check is turned off
38
+ latest_version = get_latest_version() if headless_settings.VERSION_CHECK else None
39
+ if latest_version and is_newer_version(latest_version, VERSION):
40
+ log(f"[yellow]⚠️ New version available[/yellow]")
41
+ log(f"Current: [bold]{normalize_version(VERSION)}[/bold] → Latest: {normalize_version(latest_version)}")
42
+ else:
43
+ log(f"[bold]Version {normalize_version(VERSION)}[/bold]")
44
+
45
+ log("[bold magenta]━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[/bold magenta]")
46
+ log(f":gift: Found [bold green]{len(headless_registry)}[/bold green] exposed models:")
47
+ for model_config in headless_registry.get_models():
48
+ model = model_config["model"]
49
+ log(f" [cyan]•[/cyan] {model._meta.verbose_name} ([dim]{model._meta.label_lower}[/dim])")
@@ -0,0 +1,146 @@
1
+ """
2
+ Django system checks for the registry and settings of Django Headless,
3
+ whatever app serves the exposed models. The checks of the REST routes
4
+ live in headless.rest.checks.
5
+
6
+ They run with runserver, migrate and manage.py check. Silence a single
7
+ check with SILENCED_SYSTEM_CHECKS, e.g. ["headless.W005"].
8
+ """
9
+
10
+ from difflib import get_close_matches
11
+
12
+ from django.apps import apps
13
+ from django.core import checks
14
+
15
+ from .registry import headless_registry
16
+ from .settings import DEFAULTS, SETTINGS_NAMESPACE, headless_settings
17
+
18
+ # Apps that serve the exposed models
19
+ EXPOSURE_APPS = ["headless.rest"]
20
+
21
+ # Parts of a field name (split on "_") that mark it as a credential
22
+ SENSITIVE_NAME_PARTS = {"password", "passwd", "pwd", "secret", "token", "otp", "salt", "apikey"}
23
+
24
+ # Consecutive parts of a field name that mark it as a credential
25
+ SENSITIVE_NAME_PAIRS = {("api", "key"), ("private", "key"), ("access", "key")}
26
+
27
+
28
+ def is_sensitive_field_name(name: str) -> bool:
29
+ """
30
+ Check whether a field name looks like a credential, such as password_hash,
31
+ access_token or api_key. Only whole parts of the name count, so names
32
+ like sort_key or tokenizer do not match.
33
+ """
34
+ parts = name.lower().split("_")
35
+ return any(part in SENSITIVE_NAME_PARTS for part in parts) or any(
36
+ pair in SENSITIVE_NAME_PAIRS for pair in zip(parts, parts[1:])
37
+ )
38
+
39
+
40
+ def _implicitly_serialized_fields(config):
41
+ """
42
+ Return the names of the fields of an exposed model that are serialized
43
+ without being named in its fields option: all forward fields minus the
44
+ excluded ones. Fields named in a fields option are a deliberate choice.
45
+ """
46
+ if config["fields"] is not None:
47
+ return []
48
+
49
+ model = config["model"]
50
+ excluded = set(config["exclude"] or [])
51
+ return [field.name for field in (*model._meta.fields, *model._meta.many_to_many) if field.name not in excluded]
52
+
53
+
54
+ @checks.register()
55
+ def check_settings(app_configs=None, **kwargs):
56
+ """W005: the HEADLESS setting has a key that is not a Django Headless setting."""
57
+ # A HEADLESS setting that is not a dictionary raises ImproperlyConfigured
58
+ # on first use, at startup, so it never reaches the checks.
59
+ warnings = []
60
+ for key in headless_settings.user_settings:
61
+ if key in DEFAULTS:
62
+ continue
63
+ matches = get_close_matches(str(key), DEFAULTS, n=1)
64
+ hint = f'Did you mean "{matches[0]}"?' if matches else "Remove it, or see the settings reference."
65
+ warnings.append(
66
+ checks.Warning(
67
+ f'{SETTINGS_NAMESPACE}["{key}"] is not a Django Headless setting, so it has no effect.',
68
+ hint=hint,
69
+ id="headless.W005",
70
+ )
71
+ )
72
+
73
+ return warnings
74
+
75
+
76
+ @checks.register()
77
+ def check_registrations(app_configs=None, **kwargs):
78
+ """
79
+ W008: a model is exposed more than once with different options.
80
+ W010: models are exposed, but no app that serves them is installed.
81
+ """
82
+ warnings = []
83
+
84
+ for label in sorted(headless_registry.get_conflicting_labels()):
85
+ warnings.append(
86
+ checks.Warning(
87
+ "This model is exposed more than once with different options, for example with both @expose() "
88
+ "and expose_model(). Only the last registration is in effect.",
89
+ hint="Expose the model once, with all its options.",
90
+ obj=headless_registry.get_model(label)["model"],
91
+ id="headless.W008",
92
+ )
93
+ )
94
+
95
+ if headless_registry.get_models() and not any(apps.is_installed(app) for app in EXPOSURE_APPS):
96
+ warnings.append(
97
+ checks.Warning(
98
+ "Models are exposed, but no app that serves them is installed, so the API has no routes.",
99
+ hint='Add "headless.rest" to INSTALLED_APPS, after "headless".',
100
+ id="headless.W010",
101
+ )
102
+ )
103
+
104
+ return warnings
105
+
106
+
107
+ @checks.register(checks.Tags.security)
108
+ def check_sensitive_fields(app_configs=None, **kwargs):
109
+ """
110
+ W006: an exposed model serializes a field that looks like a credential.
111
+ W007: a user model is exposed without a fields allowlist.
112
+ """
113
+ try:
114
+ from django.contrib.auth.base_user import AbstractBaseUser
115
+ except ImportError: # pragma: no cover
116
+ AbstractBaseUser = None
117
+
118
+ warnings = []
119
+
120
+ for config in headless_registry.get_models():
121
+ model = config["model"]
122
+
123
+ if AbstractBaseUser is not None and issubclass(model, AbstractBaseUser) and config["fields"] is None:
124
+ warnings.append(
125
+ checks.Warning(
126
+ "This user model is exposed without a fields option, so the API serializes every field "
127
+ "that is not excluded, which can include the password hash, email address and permissions.",
128
+ hint='List the fields the API may serialize, e.g. @expose(fields=["username", "first_name"]).',
129
+ obj=model,
130
+ id="headless.W007",
131
+ )
132
+ )
133
+
134
+ for name in _implicitly_serialized_fields(config):
135
+ if is_sensitive_field_name(name):
136
+ warnings.append(
137
+ checks.Warning(
138
+ f"The API serializes the field {name}, which looks like a credential.",
139
+ hint=f'Add "{name}" to the exclude option, or list it in the fields option '
140
+ "if the API should serialize it.",
141
+ obj=model,
142
+ id="headless.W006",
143
+ )
144
+ )
145
+
146
+ return warnings