Commit 838272ed authored by 20after4's avatar 20after4
Browse files

Support resolving PHIDs automatically.

parent 2eb8cf02
/__pycache__
*.csv
*.pyc
import pprint import pprint
from enum import Enum from enum import Enum
from collections import deque from collections import UserDict, UserList, deque
from collections.abc import MutableMapping, Iterable
import sqlite3 import sqlite3
con = sqlite3.connect(':memory:') con = sqlite3.connect(':memory:')
...@@ -20,29 +21,29 @@ class DataIterator(object): ...@@ -20,29 +21,29 @@ class DataIterator(object):
def __next__(self): def __next__(self):
return Data(next(self.data)) return Data(next(self.data))
class Data(object): class Data(MutableMapping):
data = None data = None
def __init__(self, data): def __init__(self, data, parent=None):
# print(type(data)) # print(type(data))
self.data = data self.data = data
if parent:
self.parent = parent
def __getattr__(self, attr): def __getattr__(self, attr):
#print(f"get:{attr}:{self.data[attr]}")
return self.__getitem__(attr) return self.__getitem__(attr)
def __getitem__(self, item): def __getitem__(self, item):
itemdata = self.data[item] itemdata = self.data[item]
if isinstance(itemdata, list): if isinstance(itemdata, (dict, UserDict)):
return DataList(itemdata) return Data(itemdata, self)
elif isinstance(itemdata, dict): elif isinstance(itemdata, (list, UserList, Iterable)):
return Data(itemdata) return DataList(itemdata, self)
else: else:
return itemdata return itemdata
def __dir__(self): def __dir__(self):
return self.data.keys() return self.data.keys() + self.__dict__.keys()
def __iter__(self): def __iter__(self):
return DataIterator(self.data) return DataIterator(self.data)
...@@ -56,6 +57,7 @@ class Data(object): ...@@ -56,6 +57,7 @@ class Data(object):
def __repr__(self): def __repr__(self):
return pprint.pformat(self.data, indent=2) return pprint.pformat(self.data, indent=2)
class DataList(Data): class DataList(Data):
def __getattr__(self, attr): def __getattr__(self, attr):
raise AttributeError() raise AttributeError()
...@@ -63,9 +65,11 @@ class DataList(Data): ...@@ -63,9 +65,11 @@ class DataList(Data):
def __dir__(self): def __dir__(self):
return dir(self.__dict__) return dir(self.__dict__)
class Token(Enum): class Token(Enum):
ATTR = 1 ATTR = 1
ITEM = 2 ITEM = 2
PARENT = 3
class QueryBuilder(object): class QueryBuilder(object):
...@@ -75,11 +79,15 @@ class QueryBuilder(object): ...@@ -75,11 +79,15 @@ class QueryBuilder(object):
self.query = deque() self.query = deque()
def __getitem__(self, key): def __getitem__(self, key):
self.query.append(Token.ITEM) self.query.append((Token.ITEM, key))
self.query.append(key)
return self return self
def __getattr__(self, key): def __getattr__(self, key):
self.query.append(Token.ATTR) self.query.append((Token.ATTR, key))
self.query.append(key)
return self return self
def parent(self):
self.query.append((Token.PARENT))
from builtins import str from builtins import str
from collections import deque from collections import UserDict, UserString, deque
from collections.abc import Iterable
import json import json
import os import os
# todo: remove dependency on requests # todo: remove dependency on requests
import requests import requests
from ddd.data import DataIterator, Data from ddd.data import DataIterator, Data
from ddd.phobjects import *
class Conduit(object): class Conduit(object):
phab_url = 'https://phabricator.wikimedia.org/api/' phab_url = 'https://phabricator.wikimedia.org/api/'
...@@ -47,6 +50,29 @@ class Conduit(object): ...@@ -47,6 +50,29 @@ class Conduit(object):
return r return r
return ConduitResult(conduit=self, res=r, method=method, args=args) return ConduitResult(conduit=self, res=r, method=method, args=args)
def edit(self, method: str, objectidentifier: str, transactions: list):
"""
Calls a conduit "edit" method which applies a list of transactions
to a specified object (specified by a phabricator identifier such as
T123, #project or a PHID of the appropriate type)
Raises an exception if something goes wrong and returns the decoded
conduit response otherwise.
"""
data = {
"parameters": {
"transactions": transactions,
"objectidentifier": objectidentifier
}
}
data = flatten_for_post(args)
data['api.token'] = self.conduit_token
r = requests.post(f"{self.phab_url}{method}", data=data)
json = r.json()
if json['error_info'] is not None:
raise ConduitException(self.conduit, self, json['error_info'])
return json
class ConduitResult(object): class ConduitResult(object):
""" """
...@@ -58,13 +84,14 @@ class ConduitResult(object): ...@@ -58,13 +84,14 @@ class ConduitResult(object):
method = None method = None
args = None args = None
data = None data = None
cursor = {} cursor = None
def __init__(self, conduit: Conduit, res: requests.Response, def __init__(self, conduit: Conduit, res: requests.Response,
method: str, args: dict): method: str, args: dict):
self.conduit = conduit self.conduit = conduit
self.method = method self.method = method
self.args = args self.args = args
self.cursor = {}
self.handle_result(res) self.handle_result(res)
def retry(self): def retry(self):
...@@ -103,6 +130,8 @@ class ConduitResult(object): ...@@ -103,6 +130,8 @@ class ConduitResult(object):
if "cursor" in self.result: if "cursor" in self.result:
self.cursor = self.result['cursor'] self.cursor = self.result['cursor']
else:
self.cursor = {}
if "data" in self.result: if "data" in self.result:
# Modern conduit methods return a result[data] and result{cursor} # Modern conduit methods return a result[data] and result{cursor}
...@@ -114,8 +143,36 @@ class ConduitResult(object): ...@@ -114,8 +143,36 @@ class ConduitResult(object):
self.data = self.result self.data = self.result
def has_more(self): def has_more(self):
after = self.cursor.get('after', None) return self.cursor.get('after', None)
return after is not None
def resolve_phids(self, data=False):
if data is False:
data = self.data
if isinstance(data, dict):
iter = data.items()
elif isinstance(data, (Iterable, list)) :
iter = enumerate(data)
else:
return
for key, val in iter:
if isPHID(val):
data[key] = PHObject.instance(val)
elif isinstance(val, (list, dict)):
self.resolve_phids(data=val)
if data is self.data:
phids = [phid for phid in PHObject.instances.keys()]
args = {'phids': phids}
res = self.conduit.request(method='phid.query', args=args, raw=True)
objs = res.json()
for key, vals in objs['result'].items():
PHObject.instances[key].update(vals)
# for attr in vals.keys():
# setattr(PHObject.instances[key], attr, vals[attr])
return res
def __iter__(self): def __iter__(self):
return DataIterator(self.data) return DataIterator(self.data)
...@@ -130,22 +187,13 @@ class ConduitResult(object): ...@@ -130,22 +187,13 @@ class ConduitResult(object):
return item in self.data return item in self.data
class ConduitException(Exception): class ConduitException(Exception):
def __init__(self, conduit: Conduit, result: ConduitResult, message: str): def __init__(self, conduit: Conduit, result: ConduitResult, message: str):
self.conduit = conduit self.conduit = conduit
self.result = result self.result = result
self.message = message self.message = message
def PHIDType(phid):
_, phidtype, phidhash = phid.split('-', 3)
return phidtype
def isPHID(value):
return isinstance(value, str) and str.startswith("PHID-")
def flatten_for_post(h, result=None, kk=None): def flatten_for_post(h, result=None, kk=None):
""" """
Since phab expects x-url-encoded form post data (meaning each Since phab expects x-url-encoded form post data (meaning each
......
from enum import Enum
from typing import ClassVar
"""
Phabricator Objects
PHObject and its subclasses are proxy objects that model
the relationships among objects in Phabricator's database.
Phabricator defines the relationships between objects by using PHIDs
which are long semi-structured strings that uniquely identify objects
and also carry simple type information about the object.
A PHID is made up of the string 'PHID' followed by several tokens
delimited by a '-', the first token indicates the type with 4 uppercase
ascii letters, the rest of the PHID is an arbitrary unique identifier string
Some known PHID types are enumerated in PHIDTypes class below.
"""
class PHIDTypes(Enum):
PHOB = "PHObject"
PROJ = "Project"
TASK = "Task"
CMIT = "Commit"
XACT = "Transaction"
STRY = "FeedStory"
APPS = "Application"
PCOL = "ProjectColumn"
def isPHID(value):
return isinstance(value, str) and value.startswith("PHID-")
def PHIDType(phid):
parts = phid.split('-')
phidtype = parts[1]
if phidtype in PHIDTypes.__members__:
classname = PHIDTypes[phidtype].value
return classname
return phidtype
class PHObject(object):
"""
PHObjects represent Phabricator objects such as Users and Tasks.
This class handles caching and insures that there is at most one instance
per unique phid.
"""
id: int = 0
phid: str = ""
name: str = ""
dateCreated: int = 0
dateModified: int = 0
instances: ClassVar[dict] = {}
subclasses: ClassVar[dict] = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
__class__.subclasses[cls.__name__] = cls
def __init__(self, phid, **kwargs):
self.phid = phid
self.__dict__.update(kwargs)
def __str__(self):
return self.fullName
def __repr__(self):
return f"<{self.__module__}.{self.__class__.__name__}{self.__dict__}>"
def update(self, data):
self.__dict__.update(data)
@property
def loaded(self):
return len(self.__dict__.keys()) > 1
@classmethod
def instance(cls, phid):
if phid in __class__.instances:
return __class__.instances[phid]
phidtype = PHIDType(phid)
typecls = __class__.subclasses.get(phidtype, cls)
newinstance = typecls(phid)
__class__.instances[phid] = newinstance
return newinstance
class User(PHObject):
pass
class Project(PHObject):
pass
class ProjectColumn(PHObject):
pass
class Task(PHObject):
pass
class Transaction(PHObject):
pass
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment