Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add contact information to Swagger UI (#6205) #6720

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lambdas/indexer/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"info": {
"title": "azul-indexer-dev",
"version": "3.1",
"description": "\nThis is the internal API for Azul's indexer component.\n"
"description": "\nThis is the internal API for Azul's indexer component.\n\n\n## Contact us\n\nFor technical support please file an issue at\n[GitHub](https://github.com/DataBiosphere/azul/issues) or email\n`azul-group@ucsc.edu`. To report a security concern or misconduct please email\n`azul-group@ucsc.edu`.\n"
},
"paths": {
"/": {
Expand Down
2 changes: 1 addition & 1 deletion lambdas/service/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"info": {
"title": "azul-service-dev",
"version": "12.1",
"description": "\n# Overview\n\nAzul is a REST web service for querying metadata associated with\nboth experimental and analysis data from a data repository. In order\nto deliver response times that make it suitable for interactive use\ncases, the set of metadata properties that it exposes for sorting,\nfiltering, and aggregation is limited. Azul provides a uniform view\nof the metadata over a range of diverse schemas, effectively\nshielding clients from changes in the schemas as they occur over\ntime. It does so, however, at the expense of detail in the set of\nmetadata properties it exposes and in the accuracy with which it\naggregates them.\n\nAzul denormalizes and aggregates metadata into several different\nindices for selected entity types. Metadata entities can be queried\nusing the [Index](#operations-tag-Index) endpoints.\n\nA set of indices forms a catalog. There is a default catalog called\n`dcp2` which will be used unless a\ndifferent catalog name is specified using the `catalog` query\nparameter. Metadata from different catalogs is completely\nindependent: a response obtained by querying one catalog does not\nnecessarily correlate to a response obtained by querying another\none. Two catalogs can contain metadata from the same sources or\ndifferent sources. It is only guaranteed that the body of a\nresponse by any given endpoint adheres to one schema,\nindependently of which catalog was specified in the request.\n\nAzul provides the ability to download data and metadata via the\n[Manifests](#operations-tag-Manifests) endpoints. The\n`curl` format manifests can be used to\ndownload data files. Other formats provide various views of the\nmetadata. Manifests can be generated for a selection of files using\nfilters. These filters are interchangeable with the filters used by\nthe [Index](#operations-tag-Index) endpoints.\n\nAzul also provides a [summary](#operations-Index-get_index_summary)\nview of indexed data.\n\n## Data model\n\nAny index, when queried, returns a JSON array of hits. Each hit\nrepresents a metadata entity. Nested in each hit is a summary of the\nproperties of entities associated with the hit. An entity is\nassociated either by a direct edge in the original metadata graph,\nor indirectly as a series of edges. The nested properties are\ngrouped by the type of the associated entity. The properties of all\ndata files associated with a particular sample, for example, are\nlisted under `hits[*].files` in a `/index/samples` response. It is\nimportant to note that while each _hit_ represents a discrete\nentity, the properties nested within that hit are the result of an\naggregation over potentially many associated entities.\n\nTo illustrate this, consider a data file that is part of two\nprojects (a project is a group of related experiments, typically by\none laboratory, institution or consortium). Querying the `files`\nindex for this file yields a hit looking something like:\n\n```\n{\n \"projects\": [\n {\n \"projectTitle\": \"Project One\"\n \"laboratory\": ...,\n ...\n },\n {\n \"projectTitle\": \"Project Two\"\n \"laboratory\": ...,\n ...\n }\n ],\n \"files\": [\n {\n \"format\": \"pdf\",\n \"name\": \"Team description.pdf\",\n ...\n }\n ]\n}\n```\n\nThis example hit contains two kinds of nested entities (a hit in an\nactual Azul response will contain more): There are the two projects\nentities, and the file itself. These nested entities contain\nselected metadata properties extracted in a consistent way. This\nmakes filtering and sorting simple.\n\nAlso notice that there is only one file. When querying a particular\nindex, the corresponding entity will always be a singleton like\nthis.\n"
"description": "\n# Overview\n\nAzul is a REST web service for querying metadata associated with\nboth experimental and analysis data from a data repository. In order\nto deliver response times that make it suitable for interactive use\ncases, the set of metadata properties that it exposes for sorting,\nfiltering, and aggregation is limited. Azul provides a uniform view\nof the metadata over a range of diverse schemas, effectively\nshielding clients from changes in the schemas as they occur over\ntime. It does so, however, at the expense of detail in the set of\nmetadata properties it exposes and in the accuracy with which it\naggregates them.\n\nAzul denormalizes and aggregates metadata into several different\nindices for selected entity types. Metadata entities can be queried\nusing the [Index](#operations-tag-Index) endpoints.\n\nA set of indices forms a catalog. There is a default catalog called\n`dcp2` which will be used unless a\ndifferent catalog name is specified using the `catalog` query\nparameter. Metadata from different catalogs is completely\nindependent: a response obtained by querying one catalog does not\nnecessarily correlate to a response obtained by querying another\none. Two catalogs can contain metadata from the same sources or\ndifferent sources. It is only guaranteed that the body of a\nresponse by any given endpoint adheres to one schema,\nindependently of which catalog was specified in the request.\n\nAzul provides the ability to download data and metadata via the\n[Manifests](#operations-tag-Manifests) endpoints. The\n`curl` format manifests can be used to\ndownload data files. Other formats provide various views of the\nmetadata. Manifests can be generated for a selection of files using\nfilters. These filters are interchangeable with the filters used by\nthe [Index](#operations-tag-Index) endpoints.\n\nAzul also provides a [summary](#operations-Index-get_index_summary)\nview of indexed data.\n\n## Data model\n\nAny index, when queried, returns a JSON array of hits. Each hit\nrepresents a metadata entity. Nested in each hit is a summary of the\nproperties of entities associated with the hit. An entity is\nassociated either by a direct edge in the original metadata graph,\nor indirectly as a series of edges. The nested properties are\ngrouped by the type of the associated entity. The properties of all\ndata files associated with a particular sample, for example, are\nlisted under `hits[*].files` in a `/index/samples` response. It is\nimportant to note that while each _hit_ represents a discrete\nentity, the properties nested within that hit are the result of an\naggregation over potentially many associated entities.\n\nTo illustrate this, consider a data file that is part of two\nprojects (a project is a group of related experiments, typically by\none laboratory, institution or consortium). Querying the `files`\nindex for this file yields a hit looking something like:\n\n```\n{\n \"projects\": [\n {\n \"projectTitle\": \"Project One\"\n \"laboratory\": ...,\n ...\n },\n {\n \"projectTitle\": \"Project Two\"\n \"laboratory\": ...,\n ...\n }\n ],\n \"files\": [\n {\n \"format\": \"pdf\",\n \"name\": \"Team description.pdf\",\n ...\n }\n ]\n}\n```\n\nThis example hit contains two kinds of nested entities (a hit in an\nactual Azul response will contain more): There are the two projects\nentities, and the file itself. These nested entities contain\nselected metadata properties extracted in a consistent way. This\nmakes filtering and sorting simple.\n\nAlso notice that there is only one file. When querying a particular\nindex, the corresponding entity will always be a singleton like\nthis.\n\n\n## Contact us\n\nFor technical support please file an issue at\n[GitHub](https://github.com/DataBiosphere/azul/issues) or email\n`azul-group@ucsc.edu`. To report a security concern or misconduct please email\n`azul-group@ucsc.edu`.\n"
},
"tags": [
{
Expand Down
65 changes: 37 additions & 28 deletions scripts/generate_openapi_document.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from contextlib import (
contextmanager,
)
import json
from pathlib import (
Path,
)
from typing import (
Any,
)
from unittest.mock import (
PropertyMock,
patch,
Expand All @@ -12,6 +18,7 @@
)

from azul import (
cached_property,
config,
)
from azul.chalice import (
Expand All @@ -36,38 +43,40 @@ def main():
}

lambda_name = Path.cwd().name
assert lambda_name in config.lambda_names(), lambda_name

# To create a normalized OpenAPI document, we patch any
# deployment-specific variables that affect the document.
with (
patch_config('catalogs', catalogs),
patch_config(f'{lambda_name}_function_name', f'azul-{lambda_name}-dev'),
patch_config('enable_log_forwarding', False),
patch_config('enable_replicas', True),
patch_config('monitoring_email', 'azul-group@ucsc.edu')
):
lambda_endpoint = furl('http://localhost')
with patch.object(target=AzulChaliceApp,
attribute='base_url',
new=lambda_endpoint):
app_module = load_app_module(lambda_name)
assert app_module.app.base_url == lambda_endpoint
app_spec = app_module.app.spec()
doc_path = Path(config.project_root) / 'lambdas' / lambda_name / 'openapi.json'
with write_file_atomically(doc_path) as file:
json.dump(app_spec, file, indent=4)


@contextmanager
def patch_config(attribute_name: str, value: Any):
old_value = getattr(type(config), attribute_name)
is_property = isinstance(old_value, (property, cached_property))
with patch.object(target=type(config),
attribute='catalogs',
new_callable=PropertyMock,
return_value=catalogs):
assert config.catalogs == catalogs
with patch.object(target=config,
attribute=f'{lambda_name}_function_name',
return_value=f'azul-{lambda_name}-dev'):
assert getattr(config, f'{lambda_name}_name') == f'azul-{lambda_name}-dev'
with patch.object(target=type(config),
attribute='enable_log_forwarding',
new_callable=PropertyMock,
return_value=False):
assert not config.enable_log_forwarding
with patch.object(target=type(config),
attribute='enable_replicas',
new_callable=PropertyMock,
return_value=True):
assert config.enable_replicas
lambda_endpoint = furl('http://localhost')
with patch.object(target=AzulChaliceApp,
attribute='base_url',
new=lambda_endpoint):
app_module = load_app_module(lambda_name)
assert app_module.app.base_url == lambda_endpoint
app_spec = app_module.app.spec()
doc_path = Path(config.project_root) / 'lambdas' / lambda_name / 'openapi.json'
with write_file_atomically(doc_path) as file:
json.dump(app_spec, file, indent=4)
attribute=attribute_name,
new_callable=PropertyMock if is_property else None,
return_value=value):
new_value = getattr(config, attribute_name)
assert value == (new_value() if callable(old_value) else new_value)
yield


if __name__ == '__main__':
Expand Down
16 changes: 16 additions & 0 deletions src/azul/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
from azul.collections import (
atuple,
)
from azul.openapi import (
format_description,
)
from azul.types import (
JSON,
)
Expand Down Expand Up @@ -1516,6 +1519,19 @@ def security_contact(self) -> SecurityContact | None:
import json
return json.loads(value)

@property
def contact_us(self) -> str:
email = self.monitoring_email
return format_description(f'''

## Contact us

For technical support please file an issue at
[GitHub](https://github.com/DataBiosphere/azul/issues) or email
`{email}`. To report a security concern or misconduct please email
`{email}`.
''')

@attr.s(frozen=True, kw_only=True, auto_attribs=True)
class SlackIntegration:
workspace_id: str
Expand Down
9 changes: 8 additions & 1 deletion src/azul/chalice.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
from azul.types import (
JSON,
LambdaContext,
MutableJSON,
json_dict,
json_list,
json_str,
Expand Down Expand Up @@ -144,7 +145,7 @@ def __init__(self,
self.unit_test = unit_test
self.non_interactive_routes: set[tuple[str, str]] = set()
reject('paths' in spec, 'The top-level spec must not define paths')
self._specs = copy_json(spec)
self._specs = self._add_contact_to_spec(spec)
self._specs['paths'] = {}
# The `debug` arg controls whether tracebacks appear in error responses
super().__init__(app_name, debug=config.debug > 1, configure_logs=False)
Expand All @@ -154,6 +155,12 @@ def __init__(self,
self.register_middleware(self._api_gateway_context_middleware, 'http')
self.register_middleware(self._authentication_middleware, 'http')

def _add_contact_to_spec(self, spec: JSON) -> MutableJSON:
spec = copy_json(spec)
info = json_dict(spec.setdefault('info', {}))
info['description'] = json_str(info.get('description', '')) + config.contact_us
return spec

@property
def unqualified_app_name(self):
result, _ = config.unqualified_resource_name(self.app_name)
Expand Down
16 changes: 12 additions & 4 deletions test/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ def app(self, spec):
def test_top_level_spec(self):
spec = {'foo': 'bar'}
app = self.app(spec)
self.assertEqual({'foo': 'bar', 'paths': {}}, app._specs,
actual_spec = self._assert_info(app._specs)
self.assertEqual({'foo': 'bar', 'paths': {}}, actual_spec,
"Confirm 'paths' is added")
spec['new key'] = 'new value'
self.assertNotIn('new key', app.spec(),
Expand All @@ -65,7 +66,7 @@ def route():
'tags': [],
'servers': [{'url': 'https://fake.url/'}]
}
actual_spec = self._assert_default_spec(app.spec())
actual_spec = self._assert_default_spec(self._assert_info(app.spec()))
self.assertEqual(expected, actual_spec)

def test_just_spec(self):
Expand All @@ -87,7 +88,7 @@ def route():
'servers': [{'url': 'https://fake.url/'}]
}

actual_spec = self._assert_default_spec(app.spec())
actual_spec = self._assert_default_spec(self._assert_info(app.spec()))
self.assertEqual(expected_spec, actual_spec)

def _assert_default_spec(self, actual_spec: JSON) -> JSON:
Expand All @@ -103,6 +104,13 @@ def _assert_default_spec(self, actual_spec: JSON) -> JSON:
self.assertEqual(({}, {}), (response, responses))
return actual_spec

def _assert_info(self, actual_spec: JSON) -> JSON:
actual_spec = copy_json(actual_spec)
info = actual_spec.pop('info')
self.assertIn('Contact us', info.pop('description'))
self.assertEqual({}, info)
return actual_spec

def test_fully_annotated_override(self):
app = self.app({'foo': 'bar'})
path_spec = {
Expand Down Expand Up @@ -150,7 +158,7 @@ def route():
'tags': [],
'servers': [{'url': 'https://fake.url/'}]
}
actual_spec = self._assert_default_spec(app.spec())
actual_spec = self._assert_default_spec(self._assert_info(app.spec()))
self.assertEqual(expected_specs, actual_spec)

def test_duplicate_specs(self):
Expand Down
Loading