def _render_entity_type_list_json(www_directory_path: str, entities: Iterable[Any], entity_type_name: str, configuration: Configuration) -> None: entity_type_path = os.path.join(www_directory_path, entity_type_name) with _create_json_resource(entity_type_path) as f: url_generator = SiteUrlGenerator(configuration) data = { '$schema': StaticPathUrlGenerator(configuration).generate('schema.json#/definitions/%sCollection' % entity_type_name, absolute=True), 'collection': [] } for entity in entities: data['collection'].append(url_generator.generate( entity, 'application/json', absolute=True)) dump(data, f)
def validate(data: Any, schema_definition: str, configuration: Configuration) -> None: with open( path.join(path.dirname(__file__), 'resources', 'public', 'static', 'schema.json')) as f: schema = stdjson.load(f) # @todo Can we set the schema ID somehow without making the entire JSON schema file a Jinja2 template? schema_id = StaticPathUrlGenerator(configuration).generate('schema.json', absolute=True) schema['$id'] = schema_id ref_resolver = RefResolver(schema_id, schema) jsonschema.validate(data, schema['definitions'][schema_definition], resolver=ref_resolver)
def __init__(self, configuration: Configuration, locale: str, *args, **kwargs): stdjson.JSONEncoder.__init__(self, *args, **kwargs) self._url_generator = SiteUrlGenerator(configuration) self._static_url_generator = StaticPathUrlGenerator(configuration) self._locale = locale self._mappers = { LocalizedName: self._encode_localized_name, Place: self._encode_place, Point: self._encode_coordinates, Person: self._encode_person, PersonName: self._encode_person_name, File: self._encode_file, DerivedEvent: self._encode_event, IdentifiableEvent: self._encode_identifiable_event, Event.Type: self._encode_event_type, Presence.Role: self._encode_presence_role, Date: self._encode_date, DateRange: self._encode_date_range, Citation: self._encode_citation, Source: self._encode_source, Link: self._encode_link, Note: self._encode_note, }
def __init__(self, configuration: Configuration): self._active = False self._ancestry = Ancestry() self._configuration = configuration self._assets = FileSystem() self._dispatcher = None self._localized_url_generator = AppUrlGenerator(configuration) self._static_url_generator = StaticPathUrlGenerator(configuration) self._debug = None self._locale = None self._translations = defaultdict(gettext.NullTranslations) self._default_translations = None self._extensions = None self._activation_exit_stack = AsyncExitStack() self._jinja2_environment = None self._renderer = None self._executor = None self._locks = Locks() self._http_client = None
def __init__(self, configuration: Configuration): self._site_stack = [] self._ancestry = Ancestry() self._configuration = configuration self._assets = FileSystem(join(dirname(abspath(__file__)), 'assets')) self._dispatcher = Dispatcher() self._localized_url_generator = SiteUrlGenerator(configuration) self._static_url_generator = StaticPathUrlGenerator(configuration) self._locale = None self._translations = defaultdict(gettext.NullTranslations) self._default_translations = None self._plugins = OrderedDict() self._plugin_exit_stack = AsyncExitStack() self._init_plugins() self._init_dispatch_handlers() self._init_assets() self._init_translations() self._jinja2_environment = None self._renderer = None self._executor = None self._locks = Locks()
class JSONEncoder(stdjson.JSONEncoder): def __init__(self, configuration: Configuration, locale: str, *args, **kwargs): stdjson.JSONEncoder.__init__(self, *args, **kwargs) self._url_generator = SiteUrlGenerator(configuration) self._static_url_generator = StaticPathUrlGenerator(configuration) self._locale = locale self._mappers = { LocalizedName: self._encode_localized_name, Place: self._encode_place, Point: self._encode_coordinates, Person: self._encode_person, PersonName: self._encode_person_name, File: self._encode_file, DerivedEvent: self._encode_event, IdentifiableEvent: self._encode_identifiable_event, Event.Type: self._encode_event_type, Presence.Role: self._encode_presence_role, Date: self._encode_date, DateRange: self._encode_date_range, Citation: self._encode_citation, Source: self._encode_source, Link: self._encode_link, Note: self._encode_note, } @classmethod def get_factory(cls, configuration: Configuration, locale: str): return lambda *args, **kwargs: cls(configuration, locale, *args, ** kwargs) def default(self, o): otype = type(o) if otype in self._mappers: return self._mappers[otype](o) stdjson.JSONEncoder.default(self, o) def _generate_url(self, resource: Any): return self._url_generator.generate(resource, 'application/json', locale=self._locale) def _encode_schema(self, encoded: Dict, defintion: str) -> None: encoded['$schema'] = self._static_url_generator.generate( 'schema.json#/definitions/%s' % defintion) def _encode_described(self, encoded: Dict, described: Described) -> None: if described.description is not None: encoded['description'] = described.description encoded.update({ '@context': {}, }) encoded['@context'][ 'description'] = 'https://schema.org/description' def _encode_dated(self, encoded: Dict, dated: Dated) -> None: if dated.date is not None: encoded['date'] = dated.date def _encode_date(self, date: Date) -> Dict: encoded = {} if date.year: encoded['year'] = date.year if date.month: encoded['month'] = date.month if date.day: encoded['day'] = date.day return encoded def _encode_date_range(self, date: DateRange) -> Dict: encoded = {} if date.start: encoded['start'] = date.start if date.end: encoded['end'] = date.end return encoded def _encode_has_links(self, encoded: Dict, has_links: HasLinks) -> None: encoded['links'] = list(has_links.links) def _encode_link(self, link: Link) -> Dict: return { 'url': link.url, 'label': link.label, } def _encode_has_citations(self, encoded: Dict, has_citations: HasCitations) -> None: encoded['citations'] = [ self._generate_url(citation) for citation in has_citations.citations ] def _encode_coordinates(self, coordinates: Point) -> Dict: return { '@context': { 'latitude': 'https://schema.org/latitude', 'longitude': 'https://schema.org/longitude', }, '@type': 'https://schema.org/GeoCoordinates', 'latitude': coordinates.latitude, 'longitude': coordinates.longitude, } def _encode_localized_name(self, name: LocalizedName) -> Dict: encoded = { 'name': name.name, } if name.locale: encoded['locale'] = name.locale return encoded def _encode_place(self, place: Place) -> Dict: encoded = { '@context': { 'events': 'https://schema.org/event', 'encloses': 'https://schema.org/containsPlace', }, '@type': 'https://schema.org/Place', 'id': place.id, 'names': place.names, 'events': [self._generate_url(event) for event in place.events], 'encloses': [self._generate_url(enclosed) for enclosed in place.encloses] } self._encode_schema(encoded, 'place') self._encode_has_links(encoded, place) if place.coordinates is not None: encoded['coordinates'] = place.coordinates encoded['@context']['coordinates'] = 'https://schema.org/geo' if place.enclosed_by is not None: encoded['enclosedBy'] = self._generate_url(place.enclosed_by) encoded['@context'][ 'enclosedBy'] = 'https://schema.org/containedInPlace' return encoded def _encode_person(self, person: Person) -> Dict: encoded = { '@context': { 'parents': 'https://schema.org/parent', 'children': 'https://schema.org/child', 'siblings': 'https://schema.org/sibling', }, '@type': 'https://schema.org/Person', 'id': person.id, 'names': list(person.names), 'parents': [self._generate_url(parent) for parent in person.parents], 'children': [self._generate_url(child) for child in person.children], 'siblings': [self._generate_url(sibling) for sibling in person.siblings], 'private': person.private, 'presences': [], } for presence in person.presences: if isinstance(presence.event, Identifiable): encoded['presences'].append({ '@context': { 'event': 'https://schema.org/performerIn', }, 'role': presence.role, 'event': self._generate_url(presence.event), }) self._encode_schema(encoded, 'person') self._encode_has_citations(encoded, person) self._encode_has_links(encoded, person) return encoded def _encode_person_name(self, name: PersonName) -> Dict: encoded = {} if name.individual is not None or name.affiliation is not None: encoded.update({ '@context': {}, }) if name.individual is not None: encoded['@context']['individual'] = 'https://schema.org/givenName' encoded['individual'] = name.individual if name.affiliation is not None: encoded['@context'][ 'affiliation'] = 'https://schema.org/familyName' encoded['affiliation'] = name.affiliation return encoded def _encode_file(self, file: File) -> Dict: encoded = { 'id': file.id, 'entities': [self._generate_url(entity) for entity in file.resources], 'notes': file.notes, } self._encode_schema(encoded, 'file') if file.type is not None: encoded['type'] = file.type return encoded def _encode_event(self, event: Event) -> Dict: encoded = { '@type': 'https://schema.org/Event', 'type': event.type, 'presences': [{ '@context': { 'person': 'https://schema.org/actor', }, 'role': presence.role, 'person': self._generate_url(presence.person), } for presence in event.presences], } self._encode_schema(encoded, 'event') self._encode_dated(encoded, event) self._encode_has_citations(encoded, event) if event.place is not None: encoded['place'] = self._generate_url(event.place) encoded.update({ '@context': {}, }) encoded['@context']['place'] = 'https://schema.org/location' return encoded def _encode_identifiable_event(self, event: Event) -> Dict: encoded = self._encode_event(event) encoded['id'] = event.id return encoded def _encode_event_type(self, event_type: Event.Type) -> str: return event_type.value def _encode_presence_role(self, role: Presence.Role) -> str: return role.value def _encode_citation(self, citation: Citation) -> Dict: encoded = { '@type': 'https://schema.org/Thing', 'id': citation.id, 'source': self._generate_url(citation.source), 'facts': [] } for fact in citation.facts: if isinstance(fact, Identifiable): encoded['facts'].append(self._generate_url(fact)) self._encode_schema(encoded, 'citation') self._encode_dated(encoded, citation) return encoded def _encode_source(self, source: Source) -> Dict: encoded = { '@context': { 'name': 'https://schema.org/name', }, '@type': 'https://schema.org/Thing', 'id': source.id, 'name': source.name, 'contains': [self._generate_url(contained) for contained in source.contains], 'citations': [self._generate_url(citation) for citation in source.citations], } if source.author is not None: encoded['author'] = source.author if source.publisher is not None: encoded['publisher'] = source.publisher self._encode_schema(encoded, 'source') self._encode_dated(encoded, source) self._encode_has_links(encoded, source) if source.contained_by is not None: encoded['containedBy'] = self._generate_url(source.contained_by) return encoded def _encode_note(self, note: Note) -> Dict: return { 'text': note.text, }
def create_environment(site: Site, default_locale: Optional[str] = None) -> Environment: if default_locale is None: default_locale = site.configuration.default_locale url_generator = SiteUrlGenerator(site.configuration) template_directory_paths = list( [join(path, 'templates') for path in site.resources.paths]) environment = Environment( loader=FileSystemLoader(template_directory_paths), undefined=StrictUndefined, autoescape=select_autoescape(['html']), trim_blocks=True, lstrip_blocks=True, extensions=[ 'jinja2.ext.do', 'jinja2.ext.i18n', ], ) environment.install_gettext_translations(site.translations[default_locale]) environment.globals['site'] = site environment.globals['locale'] = default_locale environment.globals['plugins'] = _Plugins(site.plugins) environment.globals['EventType'] = Event.Type environment.globals['PresenceRole'] = Presence.Role environment.globals['urlparse'] = urlparse environment.filters['map'] = _filter_map environment.filters['flatten'] = _filter_flatten environment.filters['walk'] = _filter_walk environment.filters['takewhile'] = _filter_takewhile environment.filters['locale_get_data'] = lambda locale: Locale.parse( locale, '-') environment.filters[ 'negotiate_localizeds'] = lambda localizeds: negotiate_localizeds( default_locale, localizeds) environment.filters['sort_localizeds'] = contextfilter( lambda context, *args, **kwargs: _filter_sort_localizeds( context, default_locale, *args, **kwargs)) # A filter to convert any value to JSON. @contextfilter def _filter_json(context, data, indent=None): return stdjson.dumps(data, indent=indent, cls=JSONEncoder.get_factory( site.configuration, resolve_or_missing(context, 'locale'))) environment.filters['json'] = _filter_json # Override Jinja2's built-in JSON filter, which escapes the JSON for use in HTML, to use Betty's own encoder. @contextfilter def _filter_tojson(context, data, indent=None): return htmlsafe_json_dumps(data, indent=indent, dumper=lambda *args, **kwargs: _filter_json( context, *args, **kwargs)) environment.filters['tojson'] = _filter_tojson environment.tests['resource'] = lambda x: isinstance(x, Resource) environment.tests['identifiable'] = lambda x: isinstance(x, Identifiable) environment.filters['paragraphs'] = _filter_paragraphs def _filter_format_date(date: Datey): with Translations(site.translations[default_locale]): return format_datey(date, default_locale) environment.filters['format_date'] = _filter_format_date environment.filters['format_degrees'] = _filter_format_degrees environment.globals['citer'] = _Citer() def _filter_url(resource, content_type=None, locale=None, **kwargs): content_type = content_type if content_type else 'text/html' locale = locale if locale else default_locale return url_generator.generate(resource, content_type, locale=locale, **kwargs) environment.filters['url'] = _filter_url environment.filters['static_url'] = StaticPathUrlGenerator( site.configuration).generate environment.filters['file'] = lambda *args: _filter_file(site, *args) environment.filters['image'] = lambda *args, **kwargs: _filter_image( site, *args, **kwargs) environment.globals['search_index'] = lambda: index(site, environment) for plugin in site.plugins.values(): if isinstance(plugin, Jinja2Provider): environment.globals.update(plugin.globals) environment.filters.update(plugin.filters) return environment
def build_specification(site: Site) -> Dict: url_generator = StaticPathUrlGenerator(site.configuration) specification = { 'openapi': '3.0.0', 'servers': [{ 'url': url_generator.generate('/', absolute=True), }], 'info': { 'title': 'Betty', 'version': 'dev' }, 'paths': {}, 'components': { 'responses': { '401': { 'description': _('Unauthorized'), 'content': { 'application/json': { 'schema': { '$ref': '#/components/schemas/betty/error', }, }, }, }, '403': { 'description': _('Forbidden'), 'content': { 'application/json': { 'schema': { '$ref': '#/components/schemas/betty/error', }, }, }, }, '404': { 'description': _('Not found'), 'content': { 'application/json': { 'schema': { '$ref': '#/components/schemas/betty/error', }, }, }, }, }, 'parameters': { 'id': { 'name': 'id', 'in': 'path', 'required': True, 'description': _('The ID for the resource to retrieve.'), 'schema': { 'type': 'string', }, }, }, 'headers': { 'Content-Language': { 'description': _('An HTTP [Content-Language](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language) header.' ), 'schema': { 'type': 'string', }, 'example': site.configuration.default_locale, }, }, 'schemas': { 'betty': { '$ref': url_generator.generate('/schema.json#/definitions'), }, }, }, } # Add resource operations. for resource in _get_resources(): if site.configuration.content_negotiation: collection_path = '/%s/' % resource.name single_path = '/%s/{id}/' % resource.name else: collection_path = '/{locale}/%s/index.json' % resource.name single_path = '/{locale}/%s/{id}/index.json' % resource.name specification['paths'].update({ collection_path: { 'get': { 'summary': resource.collection_endpoint_summary, 'responses': { '200': { 'description': resource.collection_response_description, 'content': { 'application/json': { 'schema': { '$ref': '#/components/schemas/betty/%sCollection' % resource.name, }, }, }, }, }, }, }, single_path: { 'get': { 'summary': resource.single_endpoint_summary, 'responses': { '200': { 'description': resource.single_response_description, 'content': { 'application/json': { 'schema': { '$ref': '#/components/schemas/betty/%s' % resource.name, }, }, }, }, }, }, }, }) # Add components for content negotiation. if site.configuration.content_negotiation: specification['components']['parameters']['Accept-Language'] = { 'name': 'Accept-Language', 'in': 'header', 'description': _('An HTTP [Accept-Language](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) header.' ), 'schema': { 'type': 'string', }, 'example': site.configuration.locales[ site.configuration.default_locale].alias, } specification['components']['schemas']['html'] = { 'type': 'string', 'description': _('An HTML5 document.'), } else: specification['components']['parameters']['locale'] = { 'name': 'locale', 'in': 'path', 'required': True, 'description': _('A locale name.'), 'schema': { 'type': 'string', 'enum': list(site.configuration.locales.keys()) }, 'example': site.configuration.locales[ site.configuration.default_locale].alias, } # Add default behavior to all requests. for path in specification['paths']: specification['paths'][path]['get']['responses'].update({ '401': { '$ref': '#/components/responses/401', }, '403': { '$ref': '#/components/responses/403', }, '404': { '$ref': '#/components/responses/404', }, }) if site.configuration.content_negotiation: specification['paths'][path]['parameters'] = [ { '$ref': '#/components/parameters/Accept-Language', }, ] else: specification['paths'][path]['parameters'] = [ { '$ref': '#/components/parameters/locale', }, ] # Add default behavior to all responses. if site.configuration.content_negotiation: responses = list(specification['components']['responses'].values()) for path in specification['paths']: responses.append( specification['paths'][path]['get']['responses']['200']) for response in responses: response['content']['text/html'] = { 'schema': { '$ref': '#/components/schemas/html' } } if 'headers' not in response: response['headers'] = {} response['headers']['Content-Language'] = { '$ref': '#/components/headers/Content-Language', } return specification