Skip to content

Instantly share code, notes, and snippets.

@toolness
Last active June 21, 2018 15:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save toolness/9673aee0b3c7a5fc37d7a00b2c157fa8 to your computer and use it in GitHub Desktop.
Save toolness/9673aee0b3c7a5fc37d7a00b2c157fa8 to your computer and use it in GitHub Desktop.
Calc API lazy docs attempt
diff --git a/api/views.py b/api/views.py
index 50d104a6..ca91ba63 100644
--- a/api/views.py
+++ b/api/views.py
@@ -43,6 +43,11 @@ SORTABLE_CONTRACT_FIELDS = list(set(
).intersection(set([f.name for f in Contract._meta.fields if f.db_index])))
+class AutoTemplatedSchema(AutoSchema):
+ def get_description(self, path, method):
+ return 'lolll'
+
+
def queryarg(name, _type, description):
'''
A simple helper method to generate a coreapi field out of a
@@ -381,7 +386,7 @@ class GetRates(APIView):
# documentation about our pagination query args.
pagination_class = ContractPagination
- schema = AutoSchema(
+ schema = AutoTemplatedSchema(
manual_fields=[
queryarg(
"contract-year",
@@ -467,7 +472,7 @@ class GetRatesCSV(APIView):
matched records.
"""
- schema = AutoSchema(
+ schema = AutoTemplatedSchema(
manual_fields=GET_CONTRACTS_QUERYARGS
)
@@ -540,7 +545,7 @@ class GetAutocomplete(APIView):
* `count` is the number of records with the labor category.
"""
- schema = AutoSchema(
+ schema = AutoTemplatedSchema(
manual_fields=[
Q_QUERYARG,
queryarg(
diff --git a/calc/settings.py b/calc/settings.py
index 2b8b6756..61c7d169 100644
--- a/calc/settings.py
+++ b/calc/settings.py
@@ -231,6 +231,7 @@ PAGINATION = 200
REST_FRAMEWORK = {
'COERCE_DECIMAL_TO_STRING': False,
+ 'DEFAULT_SCHEMA_CLASS': 'api.views.AutoTemplatedSchema',
}
LOGGING: Dict[str, Any] = {
diff --git a/api/urls.py b/api/urls.py
index 7509b258..65a55155 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -5,10 +5,10 @@ from rest_framework.documentation import include_docs_urls
from api import views
urlpatterns = [
- url(r'^rates/$', views.GetRates.as_view()),
+ url(r'^rates/$', views.GetRates.as_view(), name='api_rates'),
url(r'^rates/csv/$', views.GetRatesCSV.as_view()),
url(r'^search/$', views.GetAutocomplete.as_view()),
- url(r'^schedules/$', views.ScheduleMetadataList.as_view()),
+ url(r'^schedules/$', views.ScheduleMetadataList.as_view(), name='api_schedules'),
url(r'^docs/', include_docs_urls(
title='CALC API',
description=views.DOCS_DESCRIPTION,
diff --git a/api/views.py b/api/views.py
index 50d104a6..fa7c9c2d 100644
--- a/api/views.py
+++ b/api/views.py
@@ -1,10 +1,13 @@
import csv
from decimal import Decimal
from textwrap import dedent
+from functools import lru_cache
from django.http import HttpResponse
from django.db.models import Avg, Max, Min, Count, Q, StdDev
from django.utils.safestring import SafeString
+from django.utils.functional import lazy
+from django.template import Template, Context
from markdown import markdown
from rest_framework import serializers
@@ -43,6 +46,15 @@ SORTABLE_CONTRACT_FIELDS = list(set(
).intersection(set([f.name for f in Contract._meta.fields if f.db_index])))
+def lazy_templated_string(text):
+ @lru_cache(maxsize=1)
+ def wrapper():
+ template = Template(text)
+ context = Context()
+ return template.render(context)
+ return lazy(wrapper, str)()
+
+
def queryarg(name, _type, description):
'''
A simple helper method to generate a coreapi field out of a
@@ -116,32 +128,18 @@ GET_CONTRACTS_QUERYARGS = [
"schedule",
str,
"""
- Filter by GSA schedule. One of the following will
- return results:
-
- * Environmental
- * AIMS
- * Logistics
- * Language Services
- * PES
- * MOBIS
- * Consolidated
- * IT Schedule 70
+ Filter by GSA schedule. See the
+ [schedules endpoint]({% url 'api_schedules' %})
+ for valid values.
""",
),
queryarg(
"sin",
str,
"""
- Filter by SIN number. Examples include:
-
- * 899 - Environmental
- * 541 - AIMS
- * 87405 - Logistics
- * 73802 - Language Services
- * 871 - PES
- * 874 - MOBIS
- * 132 - IT Schedule 70
+ Filter by SIN number. See the
+ [schedules endpoint]({% url 'api_schedules' %})
+ for example values.
Note that due to the current state of data, not all
results may be returned. For more details, see
@@ -326,7 +324,7 @@ def quantize(num, precision=2):
class GetRates(APIView):
- """
+ __doc__ = lazy_templated_string("""
Get detailed information about all labor rates that match a search query.
The JSON response contains the following keys:
@@ -351,7 +349,8 @@ class GetRates(APIView):
* `hourly_rate_year1`, `current_price`, `next_year_price`,
and `second_year_price` contain pricing information for
the labor rate.
- * `schedule` is the schedule the labor rate is under.
+ * `schedule` is the schedule the labor rate is under. See the
+ [schedules endpoint]({% url 'api_schedules' %}) for valid values.
* `sin` describes the special item numbers (SINs) the labor
rate is under. See
[#1033](https://github.com/18F/calc/issues/1033) for
@@ -375,7 +374,7 @@ class GetRates(APIView):
* `min` is the minimum price of the bin.
* `max` is the maximum price of the bin.
* `count` is the number of prices in the bin.
- """
+ """)
# The AutoSchema will introspect this to ultimately generate
# documentation about our pagination query args.
@@ -443,16 +442,17 @@ class GetRates(APIView):
class ScheduleMetadataList(generics.ListAPIView):
- """
+ __doc__ = lazy_templated_string("""
Returns an array of objects representing metadata about
Schedules offered by CALC. Each object contains the following keys:
* `schedule` is the identifier for the schedule as it appears in
- other CALC API endpoints.
+ other CALC API endpoints, such as the
+ [rates endpoint]({% url 'api_rates' %}).
* `full_name` is the full name of the schedule as it should appear
to end users.
* `sin` is the SIN number of the schedule, if one exists.
- """
+ """)
queryset = ScheduleMetadata.objects.all()
serializer_class = ScheduleMetadataSerializer
@toolness
Copy link
Author

toolness commented Jun 21, 2018

These are two attempts to make CALC's API documentation more dynamic, as part of #2018. We couldn't do this eagerly during module evaluation because it would require evaluating reversing URLs, but the module is evaluated while the URL config is being processed, which results in circular importing.

An ideal solution might be to combine both approaches, but I decided against it because the implementation was getting very complex and could be hard to maintain. I also decided that there were more important things to do on the project than continue working on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment