723 lines
27 KiB
Python
723 lines
27 KiB
Python
# Copyright (c) 2015 Russell Sim <russell.sim@gmail.com>
|
|
#
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from os import path
|
|
import re
|
|
import textwrap
|
|
import xml.sax
|
|
|
|
import prettytable
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
SECTIONS = {'API_Versions': 'api-versions',
|
|
'Database_Instances': 'database-instances',
|
|
'admin-tenants': 'admin-tenants',
|
|
'admin-tokens': 'admin-tokens',
|
|
'admin-users': 'admin-users',
|
|
'admin-versions': 'admin-versions',
|
|
'alarms': 'alarms',
|
|
'build-info': 'build-info',
|
|
'capabilities': 'capabilities',
|
|
'compute_extensions': 'extensions',
|
|
'compute_flavors': 'flavors',
|
|
'compute_image_metadata': 'image-metadata',
|
|
'compute_images': 'images',
|
|
'compute_limits': 'limits',
|
|
'compute_server-actions': 'server-actions',
|
|
'compute_server-addresses': 'server-addresses',
|
|
'compute_server_metadata': 'server-metadata',
|
|
'compute_servers': 'servers',
|
|
'compute_versions': 'versions',
|
|
'credentials-v3': 'credentials',
|
|
'database-instance-actions': 'database-instance-actions',
|
|
'databases': 'databases',
|
|
'datastores': 'datastores',
|
|
'diagnostics-v2.1': 'diagnostics',
|
|
'domains-v3': 'domains',
|
|
'endpoints-v3': 'endpoints',
|
|
'ext-backups-v2': 'ext-backups',
|
|
'extraroute-ext': 'extraroute',
|
|
'flavor-extra-specs-v2.1': 'flavor-extra-specs',
|
|
'flavors': 'flavors',
|
|
'general-info': 'general-info',
|
|
'groups-v3': 'groups',
|
|
'heat-versions': 'heat-versions',
|
|
'identity-auth-v2': 'identity-auth',
|
|
'identity-v2-versions': 'versions',
|
|
'identity_v3_OS-INHERIT-ext': 'inherit',
|
|
'identity_v3_OS-KDS-ext': 'kds',
|
|
'identity_v3_OS-OAUTH1-ext': 'oauth1',
|
|
'identity_v3_OS-TRUST-ext': 'trust',
|
|
'image-data-v2': 'image-data',
|
|
'image-schemas-v2': 'image-schemas',
|
|
'image-tags-v2': 'image-tags',
|
|
'images-v1': 'images',
|
|
'images-v2': 'images',
|
|
'keypairs-v2.1': 'keypairs',
|
|
'layer3': 'layer3',
|
|
'lbaas-v1.0': 'lbaas',
|
|
'lbaas-v2.0': 'lbaas',
|
|
'limits-v2.1': 'limits',
|
|
'members-v1': 'members',
|
|
'members-v2': 'members',
|
|
'metering-ext': 'metering',
|
|
'meters': 'meters',
|
|
'network_multi_provider-ext': 'network-multi-provider',
|
|
'network_provider-ext': 'network-provider',
|
|
'network_vlan_transparency-ext': 'network-vlan-transparency',
|
|
'networks': 'networks',
|
|
'neutron-versions-v2': 'versions',
|
|
'neutron_extensions': 'extensions',
|
|
'os-admin-actions': 'admin-actions',
|
|
'os-admin-actions-v2.1': 'admin-actions',
|
|
'os-admin-password-v2.1': 'admin-password',
|
|
'os-agents': 'agents',
|
|
'os-agents-v2.1': 'agents',
|
|
'os-aggregates': 'aggregates',
|
|
'os-aggregates-v2.1': 'aggregates',
|
|
'os-availability-zone': 'availability-zone',
|
|
'os-availability-zone-v2.1': 'availability-zone',
|
|
'os-baremetal-ext-status': 'baremetal-ext-status',
|
|
'os-block-device-mapping-v2-boot': 'block-device-mapping',
|
|
'os-cells-v2.1': 'cells',
|
|
'os-certificates': 'certificates',
|
|
'os-certificates-v2.1': 'certificates',
|
|
'os-cloudpipe': 'cloudpipe',
|
|
'os-config-drive-v2.1': 'config-drive',
|
|
'os-console-output': 'console-output',
|
|
'os-console-output-v2.1': 'console-output',
|
|
'os-consoles': 'consoles',
|
|
'os-coverage': 'coverage',
|
|
'os-create-backup-v2.1': 'create-backup',
|
|
'os-createserverext': 'createserverext',
|
|
'os-deferred-delete': 'deferred-delete',
|
|
'os-deferred-delete-v2.1': 'deferred-delete',
|
|
'os-diagnostics': 'diagnostics',
|
|
'os-disk-config': 'disk-config',
|
|
'os-evacuate-v2.1': 'evacuate',
|
|
'os-ext-az': 'ext-az',
|
|
'os-ext-img-size': 'ext-img-size',
|
|
'os-ext-ips': 'ext-ips',
|
|
'os-extended-availability-zone-v2.1': 'extended-availability-zone',
|
|
'os-extended-networks': 'extended-networks',
|
|
'os-extended-server-attributes': 'extended-server-attributes',
|
|
'os-extended-server-attributes-v2.1': 'extended-server-attributes',
|
|
'os-extended-status': 'extended-status',
|
|
'os-extended-status-v2.1': 'extended-status',
|
|
'os-fixed-ips': 'fixed-ips',
|
|
'os-flavor-access': 'flavor-access',
|
|
'os-flavor-access-v2.1': 'flavor-access',
|
|
'os-flavor-extra-specs': 'flavor-extra-specs',
|
|
'os-flavor-manage-v2.1': 'flavor-manage',
|
|
'os-flavor-rxtx': 'flavor-rxtx',
|
|
'os-flavor-rxtx-v2.1': 'flavor-rxtx',
|
|
'os-flavor-swap': 'flavor-swap',
|
|
'os-flavorextradata': 'flavorextradata',
|
|
'os-flavormanage': 'flavormanage',
|
|
'os-flavors-v2.1': 'flavors',
|
|
'os-floating-ip-dns': 'floating-ip-dns',
|
|
'os-floating-ip-dns-v2.1': 'floating-ip-dns',
|
|
'os-floating-ip-pools': 'floating-ip-pools',
|
|
'os-floating-ip-pools-v2.1': 'floating-ip-pools',
|
|
'os-floating-ips': 'floating-ips',
|
|
'os-floating-ips-bulk': 'floating-ips-bulk',
|
|
'os-floating-ips-bulk-v2.1': 'floating-ips-bulk',
|
|
'os-floating-ips-v2.1': 'floating-ips',
|
|
'os-flv-disabled': 'flv-disabled',
|
|
'os-hosts': 'hosts',
|
|
'os-hosts-v2.1': 'hosts',
|
|
'os-hypervisor-status': 'hypervisor-status',
|
|
'os-hypervisors': 'hypervisors',
|
|
'os-hypervisors-v2.1': 'hypervisors',
|
|
'os-instance-actions': 'instance-actions',
|
|
'os-instance-actions-v2.1': 'instance-actions',
|
|
'os-instance-usage-audit-log-v2.1': 'instance-usage-audit-log',
|
|
'os-interface': 'interface',
|
|
'os-keypairs': 'keypairs',
|
|
'os-ksadm-admin-ext': 'ksadm-admin',
|
|
'os-kscatalog-ext': 'kscatalog',
|
|
'os-ksec2-admin-ext': 'ksec2-admin',
|
|
'os-kss3-admin-ext': 'kss3-admin',
|
|
'os-ksvalidate-ext': 'ksvalidate',
|
|
'os-limits-v2': 'limits',
|
|
'os-metadef-namespace-v2': 'metadef-namespace',
|
|
'os-metadef-object-v2': 'metadef-object',
|
|
'os-metadef-property-v2.wadl': 'metadef-property',
|
|
'os-metadef-resourcetype-v2': 'metadef-resourcetype',
|
|
'os-metadef-schemas-v2': 'metadef-schemas',
|
|
'os-metadef-tag-v2': 'metadef-tag',
|
|
'os-migrations': 'migrations',
|
|
'os-migrations-v2.1': 'migrations',
|
|
'os-multi-server-create': 'multi-server-create',
|
|
'os-multinic-v2.1': 'multinic',
|
|
'os-multiple-create-v2.1': 'multiple-create',
|
|
'os-networks': 'networks',
|
|
'os-networks-v2.1': 'networks',
|
|
'os-qos-v2-qos-specs': 'qos-v2-qos-specs',
|
|
'os-quota-class-sets': 'quota-class-sets',
|
|
'os-quota-class-sets-v2.1': 'quota-class-sets',
|
|
'os-quota-sets': 'quota-sets',
|
|
'os-quota-sets-v2': 'quota-sets',
|
|
'os-quota-sets-v2.1': 'quota-sets',
|
|
'os-remote-consoles-v2.1': 'remote-consoles',
|
|
'os-rescue': 'rescue',
|
|
'os-scheduler-hints': 'scheduler-hints',
|
|
'os-security-group-default-rules': 'security-group-default-rules',
|
|
'os-security-groups': 'security-groups',
|
|
'os-security-groups-v2.1': 'security-groups',
|
|
'os-server-OS-EXT-IPS-MAC': 'server-ext-ips-mac',
|
|
'os-server-actions-v2.1': 'server-actions',
|
|
'os-server-groups': 'server-groups',
|
|
'os-server-groups-v2.1': 'server-groups',
|
|
'os-server-password': 'server-password',
|
|
'os-server-password-v2.1': 'server-password',
|
|
'os-server-shelve': 'server-shelve',
|
|
'os-server-start-stop': 'server-start-stop',
|
|
'os-server-usage-v2.1': 'server-usage',
|
|
'os-services': 'services',
|
|
'os-services-v2.1': 'services',
|
|
'os-shelve-v2.1': 'shelve',
|
|
'os-simple-tenant-usage': 'simple-tenant-usage',
|
|
'os-tenant-networks-v2.1': 'tenant-networks',
|
|
'os-used-limits': 'used-limits',
|
|
'os-used-limits-for-admins': 'used-limits-for-admins',
|
|
'os-virtual-interfaces': 'virtual-interfaces',
|
|
'os-virtual-interfaces-v2.1': 'virtual-interfaces',
|
|
'os-volume': 'volume',
|
|
'os-volume-manage-v2': 'volume-manage',
|
|
'os-volume-type-access-v2': 'volume-type-access',
|
|
'os-volume_attachments': 'volume-attachments',
|
|
'policies-v3': 'policies',
|
|
'port_binding-ext': 'port-binding',
|
|
'ports': 'ports',
|
|
'projects-v3': 'projects',
|
|
'quotas-ext': 'quotas',
|
|
'resources': 'resources',
|
|
'roles-v3': 'roles',
|
|
'samples': 'samples',
|
|
'security_groups': 'security-groups',
|
|
'server-ips-v2.1': 'server-ips',
|
|
'service-catalog-v3': 'service-catalog',
|
|
'service-status': 'service-status',
|
|
'shared_images_v1': 'shared-images',
|
|
'software-config': 'software-config',
|
|
'stack-actions': 'stack-actions',
|
|
'stack-events': 'stack-events',
|
|
'stack-resources': 'stack-resources',
|
|
'stack-templates': 'stack-templates',
|
|
'stacks': 'stacks',
|
|
'storage_account_services': 'storage-account-services',
|
|
'storage_container_services': 'storage-container-services',
|
|
'storage_object_services': 'storage-object-services',
|
|
'subnets': 'subnets',
|
|
'tokens-v3': 'tokens',
|
|
'user_management': 'user-management',
|
|
'users-v3': 'users',
|
|
'v1.1clusters': 'clusters',
|
|
'v1.1clustertemplate': 'clustertemplate',
|
|
'v1.1datasources': 'datasources',
|
|
'v1.1event-log': 'event-log',
|
|
'v1.1imageregistry': 'imageregistry',
|
|
'v1.1jobbinaries': 'jobbinaries',
|
|
'v1.1jobbinary-internals': 'jobbinary-internals',
|
|
'v1.1jobexecutions': 'job-executions',
|
|
'v1.1jobs': 'jobs',
|
|
'v1.1jobtypes': 'job-types',
|
|
'v1.1nodegrouptemplate': 'node-group-template',
|
|
'v1.1plugins': 'plugins',
|
|
'v2.1os-fping': 'fping',
|
|
'versions-identity-v3': 'versions-identity',
|
|
'versions-images-v2': 'versions-images',
|
|
'versions-v1': 'versions',
|
|
'versions-v2.1': 'versions',
|
|
'volume-api-v1-snapshots': 'snapshots',
|
|
'volume-api-v1-types': 'types',
|
|
'volume-api-v1-versions': 'versions',
|
|
'volume-api-v1-volumes': 'volumes',
|
|
'volume-api-v2-extensions': 'extensions',
|
|
'volume-api-v2-snapshots': 'snapshots',
|
|
'volume-api-v2-types': 'types',
|
|
'volume-api-v2-versions': 'versions',
|
|
'volume-api-v2-volumes': 'volumes',
|
|
'vpnaas-v2.0': 'vpnaas'}
|
|
|
|
|
|
VERSION_RE = re.compile('v[0-9\.]+')
|
|
WHITESPACE_RE = re.compile('[\s]+', re.MULTILINE)
|
|
TITLE_RE = re.compile(
|
|
'(.*) API (v([\d.]+) )?(\S*)[ ]*\((SUPPORTED|CURRENT|DEPRECATED|\
|
|
EXPERIMENTAL)\)')
|
|
CAPTION_RE = re.compile('[*`]*')
|
|
MARKUP_RE = re.compile('[.,:;)]+')
|
|
|
|
|
|
class TableMixin(object):
|
|
def visit_table(self, attrs):
|
|
self.__table = prettytable.PrettyTable(hrules=prettytable.ALL)
|
|
self.__table.header = False
|
|
|
|
def depart_table(self):
|
|
self.content.append('\n\n')
|
|
self.content.append(str(self.__table))
|
|
self.content.append('\n\n')
|
|
|
|
def visit_caption(self, attrs):
|
|
self.content_stack.append([])
|
|
|
|
def depart_caption(self):
|
|
content = ''.join(self.content_stack.pop()).strip()
|
|
content = CAPTION_RE.sub('', content)
|
|
content = WHITESPACE_RE.sub(' ', content)
|
|
content = '**' + content + '**'
|
|
self.content.append(content)
|
|
|
|
def visit_th(self, attrs):
|
|
self.__table.header = True
|
|
|
|
def depart_th(self):
|
|
heading = self.content.pop().strip()
|
|
self.__table.field_names.append(heading)
|
|
self.__table.align[heading] = 'l'
|
|
self.__table.valign[heading] = 't'
|
|
self.__table.max_width[heading] = 80
|
|
|
|
def visit_tr(self, attrs):
|
|
self.__row = []
|
|
|
|
def visit_td(self, attrs):
|
|
self.content_stack.append([])
|
|
|
|
def depart_td(self):
|
|
self.__row.append(''.join(self.content_stack.pop()).strip())
|
|
|
|
def depart_tr(self):
|
|
if self.__row:
|
|
columns = len(self.__table.field_names)
|
|
self.__row.extend(['' for n in range(columns - len(self.__row))])
|
|
self.__table.add_row(self.__row)
|
|
|
|
|
|
class APIChapterContentHandler(xml.sax.ContentHandler, TableMixin):
|
|
|
|
EMPHASIS = {
|
|
'bold': '**',
|
|
'italic': '*'
|
|
}
|
|
|
|
def __init__(self, filename, api_parser):
|
|
self.filename = filename
|
|
self.api_parser = api_parser
|
|
|
|
def startDocument(self):
|
|
super(APIChapterContentHandler, self).startDocument()
|
|
self.tags = {}
|
|
self.current_tag = None
|
|
|
|
# general state
|
|
self.tag_stack = []
|
|
self.attr_stack = []
|
|
self.content_stack = [[]]
|
|
self.current_emphasis = None
|
|
self.nesting = 0
|
|
self.no_space = False
|
|
self.fill_width = 67
|
|
self.wrapper = textwrap.TextWrapper(width=self.fill_width)
|
|
self.inline_markup_stack = []
|
|
self.base_indent = ' '
|
|
self.hyperlink_end = False
|
|
self.markup_end = False
|
|
|
|
@property
|
|
def content(self):
|
|
return self.content_stack[-1]
|
|
|
|
def search_stack_for(self, tag_name):
|
|
for tag, attrs in zip(reversed(self.tag_stack),
|
|
reversed(self.attr_stack)):
|
|
if tag == tag_name:
|
|
return attrs
|
|
|
|
def on_top_tag_stack(self, *args):
|
|
return self.tag_stack[-len(args):] == list(args)
|
|
|
|
def startElement(self, name, _attrs):
|
|
attrs = dict(_attrs)
|
|
|
|
self.tag_stack.append(name)
|
|
self.attr_stack.append(attrs)
|
|
|
|
if self.on_top_tag_stack('chapter', 'section', 'title'):
|
|
self.content_stack.append([])
|
|
|
|
if self.on_top_tag_stack('chapter', 'title'):
|
|
self.content_stack.append([])
|
|
|
|
if self.on_top_tag_stack('chapter', 'section'):
|
|
self.content_stack.append([])
|
|
id = attrs['xml:id']
|
|
id = SECTIONS.get(id, id)
|
|
self.current_tag = {'name': id}
|
|
self.api_parser.tags.append(self.current_tag)
|
|
|
|
if name == 'wadl:resource':
|
|
filename, resource_id = attrs['href'].split("#")
|
|
dir = path.dirname(self.filename)
|
|
filepath = path.abspath(path.join(dir, filename))
|
|
tag_name = '%s#%s' % (filepath, resource_id)
|
|
self.api_parser.resource_tags[tag_name] = self.current_tag['name']
|
|
|
|
if name == 'wadl:resources':
|
|
if 'href' in attrs:
|
|
dir = path.dirname(self.filename)
|
|
filepath = path.abspath(path.join(dir, attrs['href']))
|
|
self.api_parser.file_tags[filepath] = self.current_tag['name']
|
|
|
|
if self.on_top_tag_stack('wadl:resource', 'wadl:method'):
|
|
resource = self.search_stack_for('wadl:resource')
|
|
dir = path.dirname(self.filename)
|
|
filename = resource['href'].split("#")[0]
|
|
filepath = path.abspath(path.join(dir, filename))
|
|
method_path = filepath + attrs['href']
|
|
self.api_parser.method_tags[method_path] = self.current_tag['name']
|
|
|
|
fn = getattr(self, 'visit_%s' % name, None)
|
|
if fn:
|
|
fn(dict(_attrs))
|
|
|
|
def endElement(self, name):
|
|
content = ''.join(self.content)
|
|
|
|
if self.on_top_tag_stack('chapter', 'section', 'title'):
|
|
self.current_tag['description'] = content.strip()
|
|
self.content_stack.pop()
|
|
|
|
if self.on_top_tag_stack('chapter', 'title'):
|
|
title = content.strip()
|
|
match = TITLE_RE.match(title)
|
|
if match:
|
|
title, version, version_num, ext, state = match.groups()
|
|
else:
|
|
raise Exception("Title %s doesn't match RE" % title)
|
|
self.api_parser.title = ('%s %s' % (title, ext)).strip()
|
|
self.content_stack.pop()
|
|
|
|
if self.on_top_tag_stack('chapter', 'section'):
|
|
self.current_tag['summary'] = content.strip()
|
|
self.content_stack.pop()
|
|
|
|
self.tag_stack.pop()
|
|
self.attr_stack.pop()
|
|
|
|
fn = getattr(self, 'depart_%s' % name, None)
|
|
if fn:
|
|
fn()
|
|
|
|
def characters(self, content):
|
|
if not content:
|
|
return
|
|
# Fold up any white space into a single char
|
|
if not self.on_top_tag_stack('programlisting'):
|
|
content = WHITESPACE_RE.sub(' ', content)
|
|
|
|
if content == ' ':
|
|
return
|
|
if content[0] == '\n':
|
|
return
|
|
if self.content:
|
|
if self.content[-1].endswith('\n'):
|
|
content = ' ' * self.nesting + content.strip()
|
|
elif self.content[-1].endswith(' '):
|
|
content = content.strip()
|
|
elif (self.on_top_tag_stack('programlisting')):
|
|
content = '\n' + ' ' * self.nesting + content
|
|
elif self.no_space:
|
|
content = '' + content.strip()
|
|
elif self.hyperlink_end:
|
|
self.hyperlink_end = False
|
|
if content == '.' or content == ':':
|
|
pass
|
|
else:
|
|
content = ' ' + content.strip()
|
|
elif self.markup_end:
|
|
self.markup_end = False
|
|
if MARKUP_RE.match(content):
|
|
pass
|
|
else:
|
|
content = ' ' + content.strip()
|
|
else:
|
|
content = ' ' + content.strip()
|
|
|
|
if self.no_space is True:
|
|
self.inline_markup_stack.append(content)
|
|
else:
|
|
self.content.append(content)
|
|
|
|
def visit_listitem(self, attrs):
|
|
self.nesting = len([tag for tag in self.tag_stack
|
|
if tag == 'listitem']) - 1
|
|
if self.nesting > 0:
|
|
prev_nesting = self.nesting - 1
|
|
self.base_indent = ' ' * prev_nesting + ' '
|
|
else:
|
|
self.base_indent = ' '
|
|
|
|
self.content_stack.append([self.base_indent * self.nesting + '-'])
|
|
self.wrapper = textwrap.TextWrapper(
|
|
width=self.fill_width,
|
|
initial_indent=' ',
|
|
subsequent_indent=self.base_indent * self.nesting + ' ',)
|
|
|
|
def depart_listitem(self):
|
|
content = self.content_stack.pop()
|
|
self.content.append(''.join(content))
|
|
if self.content[-1].endswith('\n\n'):
|
|
pass
|
|
else:
|
|
self.content.append('\n')
|
|
|
|
self.nesting = len([tag for tag in self.tag_stack
|
|
if tag == 'listitem']) - 1
|
|
if self.nesting > 0:
|
|
prev_nesting = self.nesting - 1
|
|
self.base_indent = ' ' * prev_nesting + ' '
|
|
else:
|
|
self.base_indent = ' '
|
|
|
|
def depart_itemizedlist(self):
|
|
if self.search_stack_for('itemizedlist') is None:
|
|
self.wrapper = textwrap.TextWrapper(width=self.fill_width)
|
|
else:
|
|
self.wrapper = textwrap.TextWrapper(
|
|
width=self.fill_width,
|
|
initial_indent=self.base_indent * self.nesting + ' ',
|
|
subsequent_indent=self.base_indent * self.nesting + ' ',)
|
|
|
|
def depart_orderedlist(self):
|
|
if self.search_stack_for('itemizedlist') is None:
|
|
self.wrapper = textwrap.TextWrapper(width=self.fill_width)
|
|
|
|
def visit_para(self, attrs):
|
|
self.content_stack.append([''])
|
|
if self.search_stack_for('itemizedlist') is not None:
|
|
return
|
|
if self.content:
|
|
if self.content[-1].endswith('\n\n'):
|
|
pass
|
|
elif self.content[-1].endswith('\n'):
|
|
self.content.append('\n')
|
|
|
|
def depart_para(self):
|
|
content = ''.join(self.content_stack.pop()).strip()
|
|
wrapped = self.wrapper.wrap(content)
|
|
self.content.append('\n'.join(wrapped))
|
|
self.content.append('\n\n')
|
|
|
|
if self.search_stack_for('itemizedlist') is None:
|
|
pass
|
|
else:
|
|
self.wrapper = textwrap.TextWrapper(
|
|
width=self.fill_width,
|
|
initial_indent=self.base_indent * self.nesting + ' ',
|
|
subsequent_indent=self.base_indent * self.nesting + ' ',)
|
|
|
|
def visit_code(self, attrs):
|
|
self.no_space = True
|
|
|
|
def depart_code(self):
|
|
content = ' ``'
|
|
if self.content:
|
|
if self.content[-1].endswith('(') or \
|
|
self.content[-1].endswith(' '):
|
|
content = '``'
|
|
content += ' '.join(self.inline_markup_stack[0:None])
|
|
content += '``'
|
|
self.content.append(content)
|
|
self.inline_markup_stack[:] = []
|
|
self.no_space = False
|
|
self.markup_end = True
|
|
|
|
def visit_emphasis(self, attrs):
|
|
# Bold is the default emphasis
|
|
self.current_emphasis = attrs.get('role', 'bold')
|
|
self.no_space = True
|
|
|
|
def depart_emphasis(self):
|
|
content = ' ' + self.EMPHASIS[self.current_emphasis]
|
|
if self.content:
|
|
if self.content[-1].endswith('(') or \
|
|
self.content[-1].endswith(' '):
|
|
content = '' + self.EMPHASIS[self.current_emphasis]
|
|
content += ' '.join(self.inline_markup_stack[0:None])
|
|
content += self.EMPHASIS[self.current_emphasis]
|
|
self.content.append(content)
|
|
self.inline_markup_stack[:] = []
|
|
self.no_space = False
|
|
self.current_emphasis = None
|
|
self.markup_end = True
|
|
|
|
def visit_programlisting(self, attrs):
|
|
if not attrs:
|
|
self.content.append('::\n\n')
|
|
else:
|
|
self.content.append('.. code-block:: %s\n\n' % attrs['language'])
|
|
self.nesting = 3
|
|
|
|
def depart_programlisting(self):
|
|
self.nesting = 0 # no indent for blank lines
|
|
self.content.append('\n\n')
|
|
|
|
def visit_link(self, attrs):
|
|
if attrs:
|
|
self.inline_markup_stack.append(attrs['xlink:href'])
|
|
self.no_space = True
|
|
|
|
def depart_link(self):
|
|
content = ' `'
|
|
# anonymous link
|
|
if len(self.inline_markup_stack) is 1:
|
|
content += ('<%s>`__' % self.inline_markup_stack[0])
|
|
else:
|
|
content += ' '.join(self.inline_markup_stack[1:None])
|
|
content += (' <%s>`_' % self.inline_markup_stack[0])
|
|
|
|
self.content.append(content)
|
|
self.inline_markup_stack[:] = []
|
|
self.no_space = False
|
|
self.hyperlink_end = True
|
|
|
|
|
|
class APIRefContentHandler(xml.sax.ContentHandler):
|
|
|
|
def __init__(self, filename):
|
|
self.filename = filename
|
|
|
|
def startDocument(self):
|
|
self.tags = []
|
|
self.current_tag = None
|
|
self.method_tags = {}
|
|
self.resource_tags = {}
|
|
self.file_tags = {}
|
|
|
|
# general state
|
|
self.tag_stack = []
|
|
self.attr_stack = []
|
|
self.content = None
|
|
|
|
def search_stack_for(self, tag_name):
|
|
for tag, attrs in zip(reversed(self.tag_stack),
|
|
reversed(self.attr_stack)):
|
|
if tag == tag_name:
|
|
return attrs
|
|
|
|
def on_top_tag_stack(self, *args):
|
|
return self.tag_stack[-len(args):] == list(args)
|
|
|
|
def startElement(self, name, _attrs):
|
|
attrs = dict(_attrs)
|
|
self.tag_stack.append(name)
|
|
self.attr_stack.append(attrs)
|
|
self.content = []
|
|
if self.on_top_tag_stack('book'):
|
|
id = attrs['xml:id']
|
|
extensions = False
|
|
if id.endswith('-ext'):
|
|
extensions = True
|
|
id = id.rsplit('-', 1)[0]
|
|
service, version = id.rsplit('-', 1)
|
|
if service.startswith('api.openstack.org-'):
|
|
service = service.split('-', 1)[1]
|
|
if extensions:
|
|
service = service + '-extensions'
|
|
assert VERSION_RE.match(version)
|
|
self.service = service
|
|
self.version = version
|
|
if self.on_top_tag_stack('book', 'xi:include'):
|
|
filename = attrs['href']
|
|
dir = path.dirname(self.filename)
|
|
filepath = path.join(dir, filename)
|
|
ch = APIChapterContentHandler(filepath, self)
|
|
xml.sax.parse(filepath, ch)
|
|
|
|
def endElement(self, name):
|
|
self.tag_stack.pop()
|
|
self.attr_stack.pop()
|
|
|
|
def characters(self, content):
|
|
content = content.strip()
|
|
if content:
|
|
self.content.append(content)
|
|
|
|
|
|
def main1(source_file, output_dir):
|
|
log.info('Parsing %s' % source_file)
|
|
ch = APIRefContentHandler(source_file)
|
|
xml.sax.parse(source_file, ch)
|
|
os.chdir(output_dir)
|
|
output = {
|
|
'title': ch.title,
|
|
'service': ch.service,
|
|
'version': ch.version,
|
|
'tags': ch.tags,
|
|
'method_tags': ch.method_tags,
|
|
'file_tags': ch.file_tags,
|
|
'resource_tags': ch.resource_tags,
|
|
}
|
|
pathname = 'api-ref-%s-%s.json' % (ch.service,
|
|
ch.version)
|
|
with open(pathname, 'w') as out_file:
|
|
json.dump(output, out_file, indent=2, sort_keys=True)
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
parser.add_argument(
|
|
'-v', '--verbose', action='count', default=0,
|
|
help="Increase verbosity (specify multiple times for more)")
|
|
parser.add_argument(
|
|
'-o', '--output-dir', action='store',
|
|
help="The directory to output the JSON files too.")
|
|
parser.add_argument(
|
|
'filename',
|
|
help="File to convert")
|
|
|
|
args = parser.parse_args()
|
|
|
|
log_level = logging.WARNING
|
|
if args.verbose == 1:
|
|
log_level = logging.INFO
|
|
elif args.verbose >= 2:
|
|
log_level = logging.DEBUG
|
|
|
|
logging.basicConfig(
|
|
level=log_level,
|
|
format='%(asctime)s %(name)s %(levelname)s %(message)s')
|
|
|
|
filename = path.abspath(args.filename)
|
|
|
|
main1(filename, output_dir=args.output_dir)
|