source: OpenRLabs-Git/deploy/rlabs-docker/web2py-rlabs/gluon/contrib/pysimplesoap/helpers.py

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100755
File size: 26.8 KB
Line 
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU Lesser General Public License as published by
5# the Free Software Foundation; either version 3, or (at your option) any
6# later version.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11# for more details.
12
13"""Pythonic simple SOAP Client helpers"""
14
15
16from __future__ import unicode_literals
17import sys
18if sys.version > '3':
19    basestring = unicode = str
20
21import datetime
22from decimal import Decimal
23import os
24import logging
25import hashlib
26import warnings
27
28try:
29    import urllib2
30    from urlparse import urlsplit
31except ImportError:
32    from urllib import request as urllib2
33    from urllib.parse import urlsplit
34
35from . import __author__, __copyright__, __license__, __version__
36
37
38log = logging.getLogger(__name__)
39
40
41def fetch(url, http, cache=False, force_download=False, wsdl_basedir='', headers={}):
42    """Download a document from a URL, save it locally if cache enabled"""
43
44    # check / append a valid schema if not given:
45    url_scheme, netloc, path, query, fragment = urlsplit(url)
46    if not url_scheme in ('http', 'https', 'file'):
47        for scheme in ('http', 'https', 'file'):
48            try:
49                path = os.path.normpath(os.path.join(wsdl_basedir, url))
50                if not url.startswith("/") and scheme in ('http', 'https'):
51                    tmp_url = "%s://%s" % (scheme, path)
52                else:
53                    tmp_url = "%s:%s" % (scheme, path)
54                log.debug('Scheme not found, trying %s' % scheme)
55                return fetch(tmp_url, http, cache, force_download, wsdl_basedir, headers)
56            except Exception as e:
57                log.error(e)
58        raise RuntimeError('No scheme given for url: %s' % url)
59
60    # make md5 hash of the url for caching...
61    filename = '%s.xml' % hashlib.md5(url.encode('utf8')).hexdigest()
62    if isinstance(cache, basestring):
63        filename = os.path.join(cache, filename)
64    if cache and os.path.exists(filename) and not force_download:
65        log.info('Reading file %s' % filename)
66        f = open(filename, 'r')
67        xml = f.read()
68        f.close()
69    else:
70        if url_scheme == 'file':
71            log.info('Fetching url %s using urllib2' % url)
72            f = urllib2.urlopen(url)
73            xml = f.read()
74        else:
75            log.info('GET %s using %s' % (url, http._wrapper_version))
76            response, xml = http.request(url, 'GET', None, headers)
77        if cache:
78            log.info('Writing file %s' % filename)
79            if not os.path.isdir(cache):
80                os.makedirs(cache)
81            f = open(filename, 'w')
82            f.write(xml)
83            f.close()
84    return xml
85
86
87def sort_dict(od, d):
88    """Sort parameters (same order as xsd:sequence)"""
89    if isinstance(od, dict):
90        ret = Struct()
91        for k in od.keys():
92            v = d.get(k)
93            # don't append null tags!
94            if v is not None:
95                if isinstance(v, dict):
96                    v = sort_dict(od[k], v)
97                elif isinstance(v, list):
98                    v = [sort_dict(od[k][0], v1) for v1 in v]
99                ret[k] = v
100        if hasattr(od, 'namespaces'):
101            ret.namespaces.update(od.namespaces)
102            ret.references.update(od.references)
103            ret.qualified = od.qualified
104        return ret
105    else:
106        return d
107
108
109def make_key(element_name, element_type, namespace):
110    """Return a suitable key for elements"""
111    # only distinguish 'element' vs other types
112    if element_type in ('complexType', 'simpleType'):
113        eltype = 'complexType'
114    else:
115        eltype = element_type
116    if eltype not in ('element', 'complexType', 'simpleType'):
117        raise RuntimeError("Unknown element type %s = %s" % (element_name, eltype))
118    return (element_name, eltype, namespace)
119
120
121def process_element(elements, element_name, node, element_type, xsd_uri,
122                    dialect, namespace, qualified=None,
123                    soapenc_uri='http://schemas.xmlsoap.org/soap/encoding/',
124                    struct=None):
125    """Parse and define simple element types as Struct objects"""
126
127    log.debug('Processing element %s %s' % (element_name, element_type))
128
129    # iterate over inner tags of the element definition:
130    for tag in node:
131
132        # sanity checks (skip superfluous xml tags, resolve aliases, etc.):
133        if tag.get_local_name() in ('annotation', 'documentation'):
134            continue
135        elif tag.get_local_name() in ('element', 'restriction', 'list'):
136            log.debug('%s has no children! %s' % (element_name, tag))
137            children = tag  # element "alias"?
138            alias = True
139        elif tag.children():
140            children = tag.children()
141            alias = False
142        else:
143            log.debug('%s has no children! %s' % (element_name, tag))
144            continue  # TODO: abstract?
145
146        # check if extending a previous processed element ("extension"):
147        new_struct = struct is None
148        if new_struct:
149            struct = Struct()
150            struct.namespaces[None] = namespace   # set the default namespace
151            struct.qualified = qualified
152
153        # iterate over the element's components (sub-elements):
154        for e in children:
155
156            # extract type information from xml attributes / children:
157            t = e['type']
158            if not t:
159                t = e['itemType']  # xs:list
160            if not t:
161                t = e['base']  # complexContent (extension)!
162            if not t:
163                t = e['ref']   # reference to another element
164            if not t:
165                # "anonymous" elements had no type attribute but children
166                if e['name'] and e.children():
167                    # create a type name to process the children
168                    t = "%s_%s" % (element_name, e['name'])
169                    c = e.children()
170                    et = c.get_local_name()
171                    c = c.children()
172                    process_element(elements, t, c, et, xsd_uri, dialect,
173                                    namespace, qualified)
174                else:
175                    t = 'anyType'  # no type given!
176
177            # extract namespace uri and type from xml attribute:
178            t = t.split(":")
179            if len(t) > 1:
180                ns, type_name = t
181            else:
182                ns, type_name = None, t[0]
183            uri = ns and e.get_namespace_uri(ns) or xsd_uri
184
185            # look for the conversion function (python type)
186            if uri in (xsd_uri, soapenc_uri) and type_name != 'Array':
187                # look for the type, None == any
188                fn = REVERSE_TYPE_MAP.get(type_name, None)
189                if tag.get_local_name() == 'list':
190                    # simple list type (values separated by spaces)
191                    fn = lambda s: [fn(v) for v in s.split(" ")]
192            elif (uri == soapenc_uri and type_name == 'Array'):
193                # arrays of simple types (look at the attribute tags):
194                fn = []
195                for a in e.children():
196                    for k, v in a[:]:
197                        if k.endswith(":arrayType"):
198                            type_name = v
199                            fn_namespace = None
200                            if ":" in type_name:
201                                fn_uri, type_name = type_name.split(":")
202                                fn_namespace = e.get_namespace_uri(fn_uri)
203                            if "[]" in type_name:
204                                type_name = type_name[:type_name.index("[]")]
205                            # get the scalar conversion function (if any)
206                            fn_array = REVERSE_TYPE_MAP.get(type_name, None)
207                            if fn_array is None and type_name != "anyType" and fn_namespace:
208                                # get the complext element:
209                                ref_type = "complexType"
210                                key = make_key(type_name, ref_type, fn_namespace)
211                                fn_complex = elements.setdefault(key, Struct(key))
212                                # create an indirect struct {type_name: ...}:
213                                fn_array = Struct(key)
214                                fn_array[type_name] = fn_complex
215                                fn_array.namespaces[None] = fn_namespace   # set the default namespace
216                                fn_array.qualified = qualified
217                            fn.append(fn_array)
218            else:
219                # not a simple python type / conversion function not available
220                fn = None
221
222            if not fn:
223                # simple / complex type, postprocess later
224                if ns:
225                    fn_namespace = uri       # use the specified namespace
226                else:
227                    fn_namespace = namespace # use parent namespace (default)
228                for k, v in e[:]:
229                    if k.startswith("xmlns:"):
230                        # get the namespace uri from the element
231                        fn_namespace = v
232                # create and store an empty python element (dict) filled later
233                if not e['ref']:
234                    ref_type = "complexType"
235                else:
236                    ref_type = "element"
237                key = make_key(type_name, ref_type, fn_namespace)
238                fn = elements.setdefault(key, Struct(key))
239
240            if e['maxOccurs'] == 'unbounded' or (uri == soapenc_uri and type_name == 'Array'):
241                # it's an array... TODO: compound arrays? and check ns uri!
242                if isinstance(fn, Struct):
243                    if len(children) > 1 or (dialect in ('jetty', )):
244                        # Jetty style support
245                        # {'ClassName': [{'attr1': val1, 'attr2': val2}]
246                        fn.array = True
247                    else:
248                        # .NET style now matches Jetty style
249                        # {'ClassName': [{'attr1': val1, 'attr2': val2}]
250                        #fn.array = True
251                        #struct.array = True
252                        fn = [fn]
253                else:
254                    if len(children) > 1 or dialect in ('jetty',):
255                        # Jetty style support
256                        # scalar array support {'attr1': [val1]}
257                        fn = [fn]
258                    else:
259                        # Jetty.NET style support (backward compatibility)
260                        # scalar array support [{'attr1': val1}]
261                        struct.array = True
262
263            # store the sub-element python type (function) in the element dict
264            if (e['name'] is not None and not alias) or e['ref']:
265                e_name = e['name'] or type_name  # for refs, use the type name
266                struct[e_name] = fn
267                struct.references[e_name] = e['ref']
268                struct.namespaces[e_name] = namespace  # set the element namespace
269            else:
270                log.debug('complexContent/simpleType/element %s = %s' % (element_name, type_name))
271                # use None to point this is a complex element reference
272                struct.refers_to = fn
273            if e is not None and e.get_local_name() == 'extension' and e.children():
274                # extend base element (if ComplexContent only!):
275                if isinstance(fn, Struct) and fn.refers_to:
276                    base_struct = fn.refers_to
277                else:
278                    # TODO: check if this actually works for SimpleContent
279                    base_struct = None
280                # extend base element:
281                process_element(elements, element_name, e.children(),
282                                element_type, xsd_uri, dialect, namespace,
283                                qualified, struct=base_struct)
284
285        # add the processed element to the main dictionary (if not extension):
286        if new_struct:
287            key = make_key(element_name, element_type, namespace)
288            elements.setdefault(key, Struct(key)).update(struct)
289
290
291def postprocess_element(elements, processed):
292    """Fix unresolved references"""
293    #elements variable contains all eelements and complexTypes defined in http://www.w3.org/2001/XMLSchema
294
295    # (elements referenced before its definition, thanks .net)
296    # avoid already processed elements:
297    if elements in processed:
298        return
299    processed.append(elements)
300
301    for k, v in elements.items():
302        if isinstance(v, Struct):
303            if v != elements:  # TODO: fix recursive elements
304                try:
305                    postprocess_element(v, processed)
306                except RuntimeError as e:  # maximum recursion depth exceeded
307                    warnings.warn(unicode(e), RuntimeWarning)
308            if v.refers_to:  # extension base?
309                if isinstance(v.refers_to, dict):
310                    extend_element(v, v.refers_to)
311                    # clean the reference:
312                    v.refers_to = None
313                else:  # "alias", just replace
314                    ##log.debug('Replacing %s = %s' % (k, v.refers_to))
315                    elements[k] = v.refers_to
316            if v.array:
317                elements[k] = [v]  # convert arrays to python lists
318        if isinstance(v, list):
319            for n in v:  # recurse list
320                if isinstance(n, (Struct, list)):
321                    #if n != elements:  # TODO: fix recursive elements
322                    postprocess_element(n, processed)
323
324def extend_element(element, base):
325    ''' Recursively extend the elemnet if it has an extension base.'''
326    ''' Recursion is needed if the extension base itself extends another element.'''
327    if isinstance(base, dict):
328        for i, kk in enumerate(base):
329            # extend base -keep orignal order-
330            if isinstance(base, Struct):
331                element.insert(kk, base[kk], i)
332                # update namespace (avoid ArrayOfKeyValueOfanyTypeanyType)
333                if isinstance(base, Struct) and base.namespaces and kk:
334                    element.namespaces[kk] = base.namespaces[kk]
335                    element.references[kk] = base.references[kk]
336        if base.refers_to:
337            extend_element(element, base.refers_to)
338
339def get_message(messages, message_name, part_name, parameter_order=None):
340    if part_name:
341        # get the specific part of the message:
342        return messages.get((message_name, part_name))
343    else:
344        # get the first part for the specified message:
345        parts = {}
346        for (message_name_key, part_name_key), message in messages.items():
347            if message_name_key == message_name:
348                parts[part_name_key] = message
349        if len(parts)>1:
350            # merge (sorted by parameter_order for rpc style)
351            new_msg = None
352            for part_name_key in parameter_order:
353                part = parts.get(part_name_key)
354                if not part:
355                    log.error('Part %s not found for %s' % (part_name_key, message_name))
356                elif not new_msg:
357                    new_msg = part.copy()
358                else:
359                    new_msg[message_name].update(part[message_name])
360            return new_msg
361        elif parts:
362            return list(parts.values())[0]
363            #return parts.values()[0]
364
365
366
367get_local_name = lambda s: s and str((':' in s) and s.split(':')[1] or s)
368get_namespace_prefix = lambda s: s and str((':' in s) and s.split(':')[0] or None)
369
370
371def preprocess_schema(schema, imported_schemas, elements, xsd_uri, dialect,
372                      http, cache, force_download, wsdl_basedir,
373                      global_namespaces=None, qualified=False):
374    """Find schema elements and complex types"""
375
376    from .simplexml import SimpleXMLElement    # here to avoid recursive imports
377
378    # analyze the namespaces used in this schema
379    local_namespaces = {}
380    for k, v in schema[:]:
381        if k.startswith("xmlns"):
382            local_namespaces[get_local_name(k)] = v
383        if k == 'targetNamespace':
384            # URI namespace reference for this schema
385            if v == "urn:DefaultNamespace":
386                v = global_namespaces[None]
387            local_namespaces[None] = v
388        if k == 'elementFormDefault':
389            qualified = (v == "qualified")
390    # add schema namespaces to the global namespace dict = {URI: ns prefix}
391    for ns in local_namespaces.values():
392        if ns not in global_namespaces:
393            global_namespaces[ns] = 'ns%s' % len(global_namespaces)
394
395    for element in schema.children() or []:
396        if element.get_local_name() in ('import', 'include',):
397            schema_namespace = element['namespace']
398            schema_location = element['schemaLocation']
399            if schema_location is None:
400                log.debug('Schema location not provided for %s!' % schema_namespace)
401                continue
402            if schema_location in imported_schemas:
403                log.debug('Schema %s already imported!' % schema_location)
404                continue
405            imported_schemas[schema_location] = schema_namespace
406            log.debug('Importing schema %s from %s' % (schema_namespace, schema_location))
407            # Open uri and read xml:
408            xml = fetch(schema_location, http, cache, force_download, wsdl_basedir)
409
410            # recalculate base path for relative schema locations
411            path = os.path.normpath(os.path.join(wsdl_basedir, schema_location))
412            path = os.path.dirname(path)
413
414            # Parse imported XML schema (recursively):
415            imported_schema = SimpleXMLElement(xml, namespace=xsd_uri)
416            preprocess_schema(imported_schema, imported_schemas, elements,
417                              xsd_uri, dialect, http, cache, force_download,
418                              path, global_namespaces, qualified)
419
420        element_type = element.get_local_name()
421        if element_type in ('element', 'complexType', "simpleType"):
422            namespace = local_namespaces[None]          # get targetNamespace
423            element_ns = global_namespaces[ns]          # get the prefix
424            element_name = element['name']
425            log.debug("Parsing Element %s: %s" % (element_type, element_name))
426            if element.get_local_name() == 'complexType':
427                children = element.children()
428            elif element.get_local_name() == 'simpleType':
429                children = element('restriction', ns=xsd_uri, error=False)
430                if not children:
431                    children = element.children()       # xs:list
432            elif element.get_local_name() == 'element' and element['type']:
433                children = element
434            else:
435                children = element.children()
436                if children:
437                    children = children.children()
438                elif element.get_local_name() == 'element':
439                    children = element
440            if children:
441                process_element(elements, element_name, children, element_type,
442                                xsd_uri, dialect, namespace, qualified)
443
444
445# simplexml utilities:
446
447try:
448    _strptime = datetime.datetime.strptime
449except AttributeError:  # python2.4
450    _strptime = lambda s, fmt: datetime.datetime(*(time.strptime(s, fmt)[:6]))
451
452
453# Functions to serialize/deserialize special immutable types:
454def datetime_u(s):
455    fmt = "%Y-%m-%dT%H:%M:%S"
456    try:
457        return _strptime(s, fmt)
458    except ValueError:
459        try:
460            # strip zulu timezone suffix or utc offset
461            if s[-1] == "Z" or (s[-3] == ":" and s[-6] in (' ', '-', '+')):
462                try:
463                    import iso8601
464                    return iso8601.parse_date(s)
465                except ImportError:
466                    pass
467
468                try:
469                    import isodate
470                    return isodate.parse_datetime(s)
471                except ImportError:
472                    pass
473
474                try:
475                    import dateutil.parser
476                    return dateutil.parser.parse(s)
477                except ImportError:
478                    pass
479
480                warnings.warn('removing unsupported "Z" suffix or UTC offset. Install `iso8601`, `isodate` or `python-dateutil` package to support it', RuntimeWarning)
481                s = s[:-1] if s[-1] == "Z" else s[:-6]
482            # parse microseconds
483            try:
484                return _strptime(s, fmt + ".%f")
485            except:
486                return _strptime(s, fmt)
487        except ValueError:
488            # strip microseconds (not supported in this platform)
489            if "." in s:
490                warnings.warn('removing unsuppported microseconds', RuntimeWarning)
491                s = s[:s.index(".")]
492            return _strptime(s, fmt)
493
494
495datetime_m = lambda dt: dt.isoformat()
496date_u = lambda s: _strptime(s[0:10], "%Y-%m-%d").date()
497date_m = lambda d: d.strftime("%Y-%m-%d")
498time_u = lambda s: _strptime(s, "%H:%M:%S").time()
499time_m = lambda d: d.strftime("%H%M%S")
500bool_u = lambda s: {'0': False, 'false': False, '1': True, 'true': True}[s]
501bool_m = lambda s: {False: 'false', True: 'true'}[s]
502decimal_m = lambda d: '{0:f}'.format(d)
503float_m = lambda f: '{0:.10f}'.format(f)
504
505# aliases:
506class Alias(object):
507    def __init__(self, py_type, xml_type):
508        self.py_type, self.xml_type = py_type, xml_type
509
510    def __call__(self, value):
511        return self.py_type(value)
512
513    def __repr__(self):
514        return "<alias '%s' for '%s'>" % (self.xml_type, self.py_type)
515
516    def __eq__(self, other):
517        return isinstance(other, Alias) and self.xml_type == other.xml_type
518       
519    def __ne__(self, other):
520        return not self.__eq__(other)
521
522    def __gt__(self, other):
523        if isinstance(other, Alias): return self.xml_type > other.xml_type
524        if isinstance(other, Struct): return False
525        return True
526
527    def __lt__(self, other):
528        if isinstance(other, Alias): return self.xml_type < other.xml_type
529        if isinstance(other, Struct): return True
530        return False
531
532    def __ge__(self, other):
533        return self.__gt__(other) or self.__eq__(other)
534
535    def __le__(self, other):
536        return self.__gt__(other) or self.__eq__(other)
537
538    def __hash__(self):
539        return hash(self.xml_type)
540
541if sys.version > '3':
542    long = Alias(int, 'long')
543byte = Alias(str, 'byte')
544short = Alias(int, 'short')
545double = Alias(float, 'double')
546integer = Alias(long, 'integer')
547DateTime = datetime.datetime
548Date = datetime.date
549Time = datetime.time
550duration = Alias(str, 'duration')
551any_uri = Alias(str, 'anyURI')
552
553# Define conversion function (python type): xml schema type
554TYPE_MAP = {
555    unicode: 'string',
556    bool: 'boolean',
557    short: 'short',
558    byte: 'byte',
559    int: 'int',
560    long: 'long',
561    integer: 'integer',
562    float: 'float',
563    double: 'double',
564    Decimal: 'decimal',
565    datetime.datetime: 'dateTime',
566    datetime.date: 'date',
567    datetime.time: 'time',
568    duration: 'duration',
569    any_uri: 'anyURI',
570}
571TYPE_MARSHAL_FN = {
572    datetime.datetime: datetime_m,
573    datetime.date: date_m,
574    datetime.time: time_m,
575    float: float_m,
576    Decimal: decimal_m,
577    bool: bool_m,
578}
579TYPE_UNMARSHAL_FN = {
580    datetime.datetime: datetime_u,
581    datetime.date: date_u,
582    datetime.time: time_u,
583    bool: bool_u,
584    str: unicode,
585}
586
587REVERSE_TYPE_MAP = dict([(v, k) for k, v in TYPE_MAP.items()])
588
589REVERSE_TYPE_MAP.update({
590    'base64Binary': str,
591    'unsignedByte': byte,
592    'unsignedInt': int,
593    'unsignedLong': long,
594    'unsignedShort': short
595})
596
597# insert str here to avoid collision in REVERSE_TYPE_MAP (i.e. decoding errors)
598if str not in TYPE_MAP:
599    TYPE_MAP[str] = 'string'
600
601
602class Struct(dict):
603    """Minimal ordered dictionary to represent elements (i.e. xsd:sequences)"""
604
605    def __init__(self, key=None):
606        self.key = key
607        self.__keys = []
608        self.array = False
609        self.namespaces = {}     # key: element, value: namespace URI
610        self.references = {}     # key: element, value: reference name
611        self.refers_to = None    # "symbolic linked" struct
612        self.qualified = None
613
614    def __setitem__(self, key, value):
615        if key not in self.__keys:
616            self.__keys.append(key)
617        dict.__setitem__(self, key, value)
618
619    def insert(self, key, value, index=0):
620        if key not in self.__keys:
621            self.__keys.insert(index, key)
622        dict.__setitem__(self, key, value)
623
624    def __delitem__(self, key):
625        if key in self.__keys:
626            self.__keys.remove(key)
627        dict.__delitem__(self, key)
628
629    def __iter__(self):
630        return iter(self.__keys)
631
632    def keys(self):
633        return self.__keys
634
635    def items(self):
636        return [(key, self[key]) for key in self.__keys]
637
638    def update(self, other):
639        if isinstance(other, Struct) and other.key:
640            self.key = other.key
641        for k, v in other.items():
642            self[k] = v
643        # do not change if we are an array but the other is not:
644        if isinstance(other, Struct) and not self.array:
645            self.array = other.array
646        if isinstance(other, Struct):
647            # TODO: check replacing default ns is a regression
648            self.namespaces.update(other.namespaces)
649            self.references.update(other.references)
650            self.qualified = other.qualified
651            self.refers_to = other.refers_to
652
653    def copy(self):
654        "Make a duplicate"
655        new = Struct(self.key)
656        new.update(self)
657        return new
658
659    def __eq__(self, other):
660        return isinstance(other, Struct) and self.key == other.key and self.key != None
661
662    def __ne__(self, other):
663        return not self.__eq__(other)
664
665    def __gt__(self, other):
666        if isinstance(other, Struct): return (self.key[2], self.key[0], self.key[1]) > (other.key[2], other.key[0], other.key[1])
667        return True
668
669    def __lt__(self, other):
670        if isinstance(other, Struct): return (self.key[2], self.key[0], self.key[1]) < (other.key[2], other.key[0], other.key[1])
671        return False
672
673    def __ge__(self, other):
674        return self.__gt__(other) or self.__eq__(other)
675
676    def __le__(self, other):
677        return self.__gt__(other) or self.__eq__(other)
678
679    def __hash__(self):
680        return hash(self.key)
681
682    def __str__(self):
683        return "%s" % dict.__str__(self)
684
685    def __repr__(self):
686        if not self.key: return str(self.keys())
687        s = '%s' % self.key[0]
688        if self.keys():
689            s += ' {'
690            for k, t in self.items():
691                is_list = False
692                if isinstance(t, list):
693                    is_list = True
694                    t = t[0]
695                if isinstance(t, type):
696                    t = t.__name__
697                    pass
698                elif isinstance(t, Alias):
699                    t = t.xml_type
700                elif isinstance(t, Struct):
701                    t = t.key[0]
702                if is_list:
703                    t = [t]
704                s += '%s: %s, ' % (k, t)
705            s = s[:-2]+'}'
706        return s
Note: See TracBrowser for help on using the repository browser.