|
14 | 14 | except:
|
15 | 15 | __version__ = 'unknown'
|
16 | 16 |
|
| 17 | +import copy |
17 | 18 | import hashlib
|
18 | 19 | import httplib
|
19 | 20 | import json
|
20 | 21 | import os.path
|
| 22 | +import re |
21 | 23 | import socket
|
22 | 24 | import time
|
23 | 25 | import urllib
|
24 | 26 | import urlparse
|
25 | 27 |
|
| 28 | +from collections import defaultdict |
| 29 | + |
26 | 30 | __all__ = ['Phabricator']
|
27 | 31 |
|
28 | 32 | # Default phabricator interfaces
|
|
34 | 38 | except IOError:
|
35 | 39 | ARCRC = None
|
36 | 40 |
|
| 41 | +# Map Phabricator types to Python types |
| 42 | +PARAM_TYPE_MAP = { |
| 43 | + # int types |
| 44 | + 'int': 'int', |
| 45 | + 'uint': 'int', |
| 46 | + 'revisionid': 'int', |
| 47 | + 'revision_id': 'int', |
| 48 | + 'diffid': 'int', |
| 49 | + 'diff_id': 'int', |
| 50 | + 'id': 'int', |
| 51 | + 'enum': 'int', |
| 52 | + |
| 53 | + # bool types |
| 54 | + 'bool': 'bool', |
| 55 | + |
| 56 | + # dict types |
| 57 | + 'map': 'dict', |
| 58 | + 'dict': 'dict', |
| 59 | + |
| 60 | + # list types |
| 61 | + 'list': 'list', |
| 62 | + |
| 63 | + # tuple types |
| 64 | + 'pair': 'tuple', |
| 65 | + |
| 66 | + # str types |
| 67 | + 'str': 'str', |
| 68 | + 'string': 'str', |
| 69 | + 'phid': 'str', |
| 70 | + 'guids': 'str', |
| 71 | + 'type': 'str', |
| 72 | +} |
| 73 | + |
| 74 | +STR_RE = re.compile(r'([a-zA-Z_]+)') |
| 75 | + |
| 76 | + |
| 77 | +def map_param_type(param_type): |
| 78 | + """ |
| 79 | + Perform param type mapping |
| 80 | + This requires a bit of logic since this isn't standardized. |
| 81 | + If a type doesn't map, assume str |
| 82 | + """ |
| 83 | + m = STR_RE.match(param_type) |
| 84 | + main_type = m.group(0) |
| 85 | + |
| 86 | + if main_type in ('list', 'array'): |
| 87 | + info = param_type.replace(' ', '').split('<', 1) |
| 88 | + |
| 89 | + # Handle no sub-type: "required list" |
| 90 | + sub_type = info[1] if len(info) > 1 else 'str' |
| 91 | + |
| 92 | + # Handle list of pairs: "optional list<pair<callsign, path>>" |
| 93 | + sub_match = STR_RE.match(sub_type) |
| 94 | + sub_type = sub_match.group(0).lower() |
| 95 | + |
| 96 | + return [PARAM_TYPE_MAP.setdefault(sub_type, 'str')] |
| 97 | + |
| 98 | + return PARAM_TYPE_MAP.setdefault(main_type, 'str') |
| 99 | + |
| 100 | + |
| 101 | +def parse_interfaces(interfaces): |
| 102 | + """ |
| 103 | + Parse the conduit.query json dict response |
| 104 | + This performs the logic of parsing the non-standard params dict |
| 105 | + and then returning a dict Resource can understand |
| 106 | + """ |
| 107 | + parsed_interfaces = defaultdict(dict) |
| 108 | + |
| 109 | + for m, d in interfaces.iteritems(): |
| 110 | + app, func = m.split('.') |
| 111 | + |
| 112 | + method = parsed_interfaces[app][func] = {} |
| 113 | + |
| 114 | + # Make default assumptions since these aren't provided by Phab |
| 115 | + method['formats'] = ['json', 'human'] |
| 116 | + method['method'] = 'POST' |
| 117 | + |
| 118 | + method['optional'] = {} |
| 119 | + method['required'] = {} |
| 120 | + |
| 121 | + for name, type_info in dict(d['params']).iteritems(): |
| 122 | + # Usually in the format: <optionality> <param_type> |
| 123 | + info_pieces = type_info.split(' ', 1) |
| 124 | + |
| 125 | + # If optionality isn't specified, assume required |
| 126 | + if info_pieces[0] not in ('optional', 'required'): |
| 127 | + optionality = 'required' |
| 128 | + param_type = info_pieces[0] |
| 129 | + # Just make an optional string for "ignored" params |
| 130 | + elif info_pieces[0] == 'ignored': |
| 131 | + optionality = 'optional' |
| 132 | + param_type = 'string' |
| 133 | + else: |
| 134 | + optionality = info_pieces[0] |
| 135 | + param_type = info_pieces[1] |
| 136 | + |
| 137 | + # This isn't validated by the client |
| 138 | + if param_type.startswith('nonempty'): |
| 139 | + optionality = 'required' |
| 140 | + param_type = param_type[9:] |
| 141 | + |
| 142 | + method[optionality][name] = map_param_type(param_type) |
| 143 | + |
| 144 | + return dict(parsed_interfaces) |
| 145 | + |
37 | 146 |
|
38 | 147 | class InterfaceNotDefined(NotImplementedError):
|
39 | 148 | pass
|
@@ -75,11 +184,19 @@ def __len__(self):
|
75 | 184 | def keys(self):
|
76 | 185 | return self.response.keys()
|
77 | 186 |
|
| 187 | + def iteritems(self): |
| 188 | + for k, v in self.response.iteritems(): |
| 189 | + yield k, v |
| 190 | + |
| 191 | + def itervalues(self): |
| 192 | + for v in self.response.itervalues(): |
| 193 | + yield v |
| 194 | + |
78 | 195 |
|
79 | 196 | class Resource(object):
|
80 |
| - def __init__(self, api, interface=INTERFACES, endpoint=None, method=None): |
| 197 | + def __init__(self, api, interface=None, endpoint=None, method=None): |
81 | 198 | self.api = api
|
82 |
| - self.interface = interface |
| 199 | + self.interface = interface or copy.deepcopy(parse_interfaces(INTERFACES)) |
83 | 200 | self.endpoint = endpoint
|
84 | 201 | self.method = method
|
85 | 202 |
|
@@ -208,3 +325,10 @@ def connect(self):
|
208 | 325 |
|
209 | 326 | def generate_hash(self, token):
|
210 | 327 | return hashlib.sha1(token + self.api.certificate).hexdigest()
|
| 328 | + |
| 329 | + def update_interfaces(self): |
| 330 | + query = Resource(api=self, method='conduit', endpoint='query') |
| 331 | + |
| 332 | + interfaces = query() |
| 333 | + |
| 334 | + self.interface = parse_interfaces(interfaces) |
0 commit comments