import urllib import urllib.request import zipfile import json import http.client import socket import ssl import time import re import io import os from PyQt5.QtCore import * from PyQt5.QtGui import * class GetKritaAPI(QObject): MAP_TYPES = { 'QStringList': ['List[str]'], 'QString': ['str'], 'qreal': ['float'], 'double': ['float'], 'QByteArray': ['Union[QByteArray, bytes, bytearray]','QByteArray'], 'QList':['List'], 'QVector':['List'], 'QMap':['Dict'], 'true':['True'], 'false':['False'], 'void': ['None'] } DECLARE_CLASS = { 'Document': 'Krita.instance().activeDocument()', 'Node': 'Krita.instance().activeDocument().activeNode()' } def updateData(self, kritaVersion, defaultTimeout = 5): headers = { 'Accept': '*/*', "User-Agent": "Krita-DevTools Plugin" } # Get List of tags and confirm if IPv6 is working properly tagList = "https://invent.kde.org/api/v4/projects/graphics%2Fkrita/repository/tags" s1 = time.time() req = urllib.request.Request(tagList, headers=headers) with urllib.request.urlopen(req, timeout=defaultTimeout) as source: if time.time() - s1 > defaultTimeout: ForceIPv4Connection() defaultTimeout += 25 try: jres = json.loads(source.read()) tag = 'master' for tagDict in jres: if (tagDict['name'].split('-'))[0] == 'v'+ kritaVersion: tag = tagDict['name'] # Figure out when API was last updated lastCommit = "https://invent.kde.org/api/graphql" urldata={"query":"query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {\n project(fullPath: $projectPath) {\n id\n __typename\n repository {\n __typename\n tree(path: $path, ref: $ref) {\n __typename\n lastCommit {\n __typename\n sha\n title\n titleHtml\n descriptionHtml\n message\n webPath\n authoredDate\n authorName\n authorGravatar\n author {\n __typename\n name\n avatarUrl\n webPath\n }\n signatureHtml\n pipelines(ref: $ref, first: 1) {\n __typename\n edges {\n __typename\n node {\n __typename\n detailedStatus {\n __typename\n detailsPath\n icon\n tooltip\n text\n group\n }\n }\n }\n }\n }\n }\n }\n }\n}\n","variables":{"projectPath":"graphics/krita","ref":tag,"path":"libs/libkis"}} req = urllib.request.Request(lastCommit, headers=headers) req.add_header('Content-Type', 'application/json; charset=utf-8') jsondata = json.dumps(urldata) jsondataasbytes = jsondata.encode('utf-8') # needs to be bytes req.add_header('Content-Length', len(jsondataasbytes)) with urllib.request.urlopen(req, jsondataasbytes, timeout=defaultTimeout) as source: jres = json.loads(source.read()) date = jres['data']['project']['repository']['tree']['lastCommit']['authoredDate'] # Download the API details zipUrl = 'https://invent.kde.org/graphics/krita/-/archive/'+tag+'/krita-'+tag+'.zip?path=libs/libkis' req = urllib.request.Request(zipUrl, headers=headers) with urllib.request.urlopen(req, timeout=defaultTimeout) as source: data = source.read() with zipfile.ZipFile(io.BytesIO(data)) as myzip: if 'krita-'+tag+'-libs-libkis/libs/libkis/Window.h' in myzip.namelist(): with open( os.path.dirname(os.path.realpath(__file__)) + '.KritaAPI.'+kritaVersion+'.zip', 'wb') as f: f.write(data) f.close() return { 'status': 1, 'data': { 'updated': date } } else: raise Exception("Archive is invalid.") except Exception as e: return { 'status':0, 'error': str(e) } def parseData(self, kritaVersion): mapDict = {} with zipfile.ZipFile( os.path.dirname(os.path.realpath(__file__)) + '.KritaAPI.'+kritaVersion+'.zip' ) as myzip: for fn in myzip.namelist(): if fn.endswith('.h') and '/Test/' not in fn: content = str(myzip.read(fn),'utf-8') className = re.search(r'^.*[/\\](.+?)\.h$', fn).group(1) #print ("CLASSNAME", className) #print ( content ) regexFile = re.compile(r"^(?:.+?\/\*\*\s*(.+?)\s*\*\/.*?|.+?)\n\s*?class KRITALIBKIS_EXPORT (\w+)\s*?[\:\n]\s*(?:public\s+|)(\w+|)", re.DOTALL) match = regexFile.search(content) content = regexFile.sub('', content) #print ( content ) regexClass = re.compile(r"\n\s*?(public|private)\s*(Q_SLOTS|)\s*:\s*?\n(.+?)(\n\s*?(?:public|private)\s*(?:Q_SLOTS|)\s*:\s*?\n|\#endif \/\/ LIBKIS_)", re.DOTALL) regexMethod = re.compile(r"^(?:.+?(?P\/\*\*\s*.+?\s*\*\/|\/\/\/[^\r\n]+?)\s*?|\s+?)\n\s*?(?:(?P(?:virtual|static|)\s*?[\w\*]+(?:\<[\w \*,]+\>|))\s+(?P[\w\*]+)|(?P"+className+r"))\((?P[^\r\n]*)\)(?P[^\r\n])*?;", re.DOTALL) regexParam = re.compile(r'(?:^|\s)(\S+?(?:\<.+?\>|))\s*[\&\*]*(\w+?)(?:\s*=\s*(.+?)|)\s*$', re.DOTALL) if match: #print( "match=",match.group(2), match.group(1) ) modName = match.group(2) doc = match.group(1) if match.group(1) else '' doc = re.sub(r'^\s*(?:/\*\*|///)\s*\**', r'', doc) doc = re.sub(r'/\s*$', r'', doc) doc = re.sub(r'\n\s*?\* \@', r'\n@', doc ) start = doc.find('@code') if start > -1: end = doc.find('@endcode',start) doc = doc.replace( doc[start:end],re.sub(r'\n\s*?\* ',r'\n', doc[start:end])) doc = re.sub(r'\n\s*?\*', r'', doc ) mapDict[modName] = { 'doc':doc, 'methods':{}, 'declare':[], 'parent':match.group(3) } if modName in self.DECLARE_CLASS: mapDict[modName]['declare'].append({ 'doc': self.DECLARE_CLASS[modName], 'return': className, 'params': [], 'access': '' }) while True: match = regexClass.search(content) if match is not None: #print ( "matchMethod", match.group(1), match.group(2), match.group(3) ) content = regexClass.sub(match.group(4), content, 1) methodAccess = match.group(1) + " " + match.group(2) subcontent = match.group(3) while True: matchMethod = regexMethod.search(subcontent) subcontent = regexMethod.sub('', subcontent, 1) if matchMethod: #if matchMethod.group('cname'): print ("CNAME!", matchMethod.group('cname') ) docMethod = matchMethod.group('doc') if matchMethod.group('doc') else '' docMethod = re.sub(r'^\s*(?:/\*\*|///)\s*\**', r'', docMethod) docMethod = re.sub(r'/\s*$', r'', docMethod) docMethod = re.sub(r'\n\s*?\* \@', r'\n@', docMethod) start2 = docMethod.find('@code') if start2 > -1: #print ("SELECTION FOUND!") end2 = docMethod.find('@endcode',start2) docMethod = docMethod.replace( docMethod[start2:end2],re.sub(r'\n\s*?\* ',r'\n', docMethod[start2:end2])) docMethod = re.sub(r'\n\s*?\*', r'', docMethod ) methName = (matchMethod.group('name') if matchMethod.group('name') else matchMethod.group('cname')).replace('*','') params = [] #print ('mp', matchMethod.group('params')) paramText = matchMethod.group('params') if paramText: paramText = re.sub(r'\<(.*?),(.*?)\>',r'<\1&&\2>', paramText) for param in paramText.split(','): pmatch = regexParam.search(param) #print ( "p["+param+']', pmatch ) params.append({ 'type': re.sub(r'\<(.*?)&&(.*?)\>',r'<\1,\2>', pmatch.group(1)), 'name': pmatch.group(2), 'optional': re.sub(r'\<(.*?)&&(.*?)\>',r'<\1,\2>', pmatch.group(3)) if pmatch.group(3) else None }) #print ("met", className, methName, params) #if 'internal use only' in docMethod: continue if methName == modName: mapDict[modName]['declare'].append({ 'doc': docMethod, 'return': className if matchMethod.group('cname') else matchMethod.group('type'), 'params': params, 'access': methodAccess }) else: mapDict[modName]['methods'][methName]={ 'name': methName, 'doc':docMethod, 'return': matchMethod.group('type').strip(), 'params': params, 'access': methodAccess } else: break #print ("cont=",content) else: #print ("NO MATCH!") break #print ("cont=",content) #methods = decl.group(3) return mapDict def convertType(self, vtype, classKeys, param = True): for k in reversed(sorted(self.MAP_TYPES.keys())): v = self.MAP_TYPES[k] rv = v[0] if param or len(v) == 1 else v[1] if k == vtype: return rv elif k in vtype: vtype = vtype.replace(k, rv) vtype = vtype.replace('<','[').replace('>',']').replace(' *','').replace('*','').replace('&','').replace('virtual ','') #if param: for k in reversed(sorted(classKeys)): vtype = re.sub( r"(^|[\[\( ,])"+k, r"\1'"+k+"'", vtype) vtype = re.sub( r"((?:Kis|Ko)\w+)", r"'\1'", vtype) return vtype def genAutoComplete(self, kritaVersion): output = "from PyQt5.QtCore import *\nfrom PyQt5.QtGui import *\nfrom PyQt5.QtWidgets import *\nfrom typing import List, Dict, Union\n" parseData = self.parseData(kritaVersion) sortedLists = { 'QObject':[], 'Other':[] } for k in reversed(sorted(parseData.keys())): v = parseData[k] if v['parent'] == 'QObject': sortedLists['QObject'].append( k ) else: sortedLists['Other'].append( k ) if v['parent'].startswith('Ko') or v['parent'].startswith('Kis'): output += v['parent']+" = QObject\n" for k in sortedLists['QObject'] + sortedLists['Other']: v = parseData[k] output += "class " + k + "(" + v['parent'] +"):\n" output += '\t"""' + v['doc'] + ' """\n\n' if v['declare']: paramList = ['self'] for param in m['params']: paramList.append( param['name'] + ": " + self.convertType(param['type'], parseData.keys() ) + (" = " + self.convertType(param['optional'], parseData.keys() ) if param['optional'] else "") ) output += "\tdef __init__(" + ', '.join(paramList) + ") -> " + self.convertType(m['return'], parseData.keys() , False) +":\n" output += '\t\t"""' + m['doc'] + ' """\n\n' for km, m in v['methods'].items(): paramList = [] paramNames = [] #print ( "METHOD=", m ) if "static" not in m['return']: paramList.append('self') else: m['return'] = m['return'].replace('static','') output += "\t@staticmethod\n" for param in m['params']: paramNames.append( param['name'] ) paramList.append( param['name'] + ": " + self.convertType(param['type'], parseData.keys() ) + (" = " + self.convertType(param['optional'], parseData.keys() ) if param['optional'] else "") ) output += "\tdef " + m['name'] + "(" + ', '.join(paramList) + ") -> " + self.convertType(m['return'], parseData.keys() , False) +":\n" output += "\t\t# type: (" + ', '.join(paramNames) + ") -> " + self.convertType(m['return'], {}.keys() , False) +":\n" output += '\t\t"""@access ' + m['access'] + "\n" + m['doc'] + ' """\n\n' return output class ForceIPv4Connection(): def __init__(self, caller = None): super().__init__() urllib.request.HTTPSHandler = self.OverloadHTTPSHandler print ("overloaded!") class OverloadHTTPSConnection(http.client.HTTPSConnection): def connect(self): self.sock = socket.socket(socket.AF_INET) self.sock.connect((self.host, self.port)) if self._tunnel_host: self._tunnel() self.sock = ssl.wrap_socket(self.sock, self.key_file, self.cert_file) class OverloadHTTPSHandler(urllib.request.AbstractHTTPHandler): def __init__(self, debuglevel=0, context=None, check_hostname=None): urllib.request.AbstractHTTPHandler.__init__(self, debuglevel) self._context = context self._check_hostname = check_hostname def https_open(self, req): return self.do_open(ForceIPv4Connection.OverloadHTTPSConnection, req, context=self._context, check_hostname=self._check_hostname) https_request = urllib.request.AbstractHTTPHandler.do_request_