c11_api.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. import base64
  2. import os
  3. import requests
  4. from requests_toolbelt.multipart import decoder
  5. import jinja2
  6. import json
  7. import re
  8. from bs4 import BeautifulSoup
  9. import config
  10. from cognos11.xml_prettify import prettify_xml
  11. import logging
  12. class c11_api:
  13. webservice = ""
  14. templates_dir = ""
  15. # templates_dir = "C:/GlobalCube/Tasks/gctools/templates"
  16. headers = {}
  17. caf = ""
  18. cam = ""
  19. reports = []
  20. folders = []
  21. jobs = []
  22. def __init__(self, cfg: config.Config):
  23. self.cfg = cfg
  24. self.webservice = cfg.cognos11.webservice
  25. self.templates_dir = cfg.tasks_dir + '/scripts/templates'
  26. self._env = jinja2.Environment(
  27. loader=jinja2.FileSystemLoader(self.templates_dir),
  28. autoescape=jinja2.select_autoescape(['html', 'xml'])
  29. )
  30. self.templates = {
  31. 'get_report': self._env.get_template('get_report.xml'),
  32. 'create_report': self._env.get_template('create_report.xml'),
  33. 'update_report': self._env.get_template('update_report.xml'),
  34. 'get_package': self._env.get_template('get_package.xml'),
  35. }
  36. @staticmethod
  37. def generate_token(message_base64):
  38. version = "V1".encode("utf-8")
  39. header_len = 4
  40. msg = base64.b64decode(message_base64)[1:]
  41. chunks = []
  42. while len(msg) >= header_len:
  43. chunk_len = int.from_bytes(msg[:header_len], byteorder="little")
  44. msg = msg[header_len:]
  45. chunks.append(msg[:chunk_len])
  46. msg = msg[chunk_len:]
  47. return base64.b64encode(version + chunks[-1]).decode("utf-8")
  48. def login(self, user='Global1', password='Cognos#11'):
  49. credentials = {
  50. "parameters": [
  51. {"name": "CAMNamespace", "value": "CognosEx"},
  52. {"name": "h_CAM_action", "value": "logonAs"},
  53. {"name": "CAMUsername", "value": user},
  54. {"name": "CAMPassword", "value": password}
  55. ]
  56. }
  57. self.session = requests.Session()
  58. r = self.session.get(self.webservice)
  59. self.headers = {'Content-Type': "application/json; charset=UTF-8", 'X-XSRF-TOKEN': self.session.cookies.get('XSRF-TOKEN')}
  60. r = self.session.post(self.webservice + "v1/login", data=json.dumps(credentials), headers=self.headers)
  61. self.caf = r.json()['cafContextId']
  62. self.cam = self.generate_token(r.cookies["usersessionid"])
  63. return self
  64. def get_folders(self):
  65. if len(self.folders) == 0:
  66. self.folders.append({'id': '_dot_public_folders', 'name': 'Team Content'})
  67. self.load_folder_list()
  68. self.save_config()
  69. return self.folders
  70. def save_config(self):
  71. os.makedirs(self.cfg.cognos11.config_dir, exist_ok=True)
  72. with open(self.cfg.cognos11.folders_file, 'w') as fwh:
  73. json.dump(self.folders, fwh, indent=2)
  74. with open(self.cfg.cognos11.reports_file, 'w') as fwh:
  75. json.dump(self.reports, fwh, indent=2)
  76. with open(self.cfg.cognos11.jobs_file, 'w') as fwh:
  77. json.dump(self.jobs, fwh, indent=2)
  78. def load_config_from_files(self):
  79. with open(self.cfg.cognos11.folders_file, 'r') as frh:
  80. self.folders = json.load(frh)
  81. with open(self.cfg.cognos11.reports_file, 'r') as frh:
  82. self.reports = json.load(frh)
  83. with open(self.cfg.cognos11.jobs_file, 'w') as frh:
  84. self.jobs = json.load(frh)
  85. def load_folder_list(self, folder_id='_dot_public_folders', prefix='Team Content'):
  86. res = self.session.get(f"{self.webservice}v1/objects/{folder_id}/items", headers=self.headers)
  87. folder_list = sorted(res.json()['data'], key=lambda x: x['defaultName'])
  88. for f in folder_list:
  89. if f['type'] == 'folder':
  90. folder = {
  91. 'id': f['id'],
  92. 'name': prefix + '/' + f['defaultName'].replace('/', '-')
  93. }
  94. self.folders.append(folder)
  95. self.load_folder_list(folder['id'], folder['name'])
  96. elif f['type'] == 'report':
  97. report = {
  98. 'id': f['id'],
  99. 'name': f['defaultName'].replace('/', '-'),
  100. 'path': prefix
  101. }
  102. self.reports.append(report)
  103. elif f['type'] == 'jobDefinition':
  104. job = {
  105. 'id': f['id'],
  106. 'name': f['defaultName'],
  107. 'path': prefix
  108. }
  109. job['details'] = self.get_job_details(job['id'])
  110. self.jobs.append(job)
  111. def get_job_details(self, job_id):
  112. fields = ",".join([
  113. 'userInterfaces,disabled',
  114. 'runInAdvancedViewer,modificationTime,canBurst',
  115. 'defaultPortalAction',
  116. 'base.defaultName,tags,target.searchPath,target.disabled',
  117. 'options,base.options'
  118. ])
  119. res = self.session.get(f"{self.webservice}v1/objects/{job_id}?fields={fields}", headers=self.headers)
  120. job = res.json()['data'][0]
  121. job.pop('_meta', None)
  122. job.pop('id', None)
  123. job.pop('type', None)
  124. job.pop('defaultName', None)
  125. fields2 = ",".join([
  126. r'id,displaySequence,stepObject{defaultName}',
  127. r'stepObject{id},stepObject{parameters}',
  128. r'stepObject{canBurst},options,parameters'
  129. ])
  130. res = self.session.get(
  131. f"{self.webservice}v1/objects/{job_id}/items?types=jobStepDefinition&fields={fields2}",
  132. headers=self.headers
  133. )
  134. steps = res.json()['data']
  135. for s in steps:
  136. s.pop('_meta', None)
  137. if s['stepObject'] is not None:
  138. s['report_id'] = s['stepObject'][0]['id']
  139. s.pop('stepObject', None)
  140. job['steps'] = steps
  141. return job
  142. def get_report(self, report_id):
  143. self.get_folders()
  144. report = [r for r in self.reports if r['id'] == report_id]
  145. if len(report) == 0:
  146. return None
  147. report = report[0]
  148. if 'meta' not in report:
  149. report = self.get_report_specs(report)
  150. return report
  151. def get_reports_in_folder(self, folder, recursive=False, specs=False):
  152. self.get_folders()
  153. if recursive:
  154. res = [r for r in self.reports if r['path'].startswith(folder)]
  155. else:
  156. res = [r for r in self.reports if r['path'] == folder]
  157. if specs:
  158. return [self.get_report_specs(r) for r in res]
  159. return res
  160. def get_report_specs(self, report):
  161. headers = {
  162. 'Content-Type': 'text/xml; charset=UTF-8',
  163. 'X-XSRF-TOKEN': self.headers['X-XSRF-TOKEN'],
  164. 'X-RsCMStoreID': report['id'],
  165. 'X-UseRsConsumerMode': 'true',
  166. 'SOAPAction': 'http://www.ibm.com/xmlns/prod/cognos/reportService/202004/'
  167. }
  168. soap = self.templates['get_report'].render(
  169. {"caf": self.caf, "cam": self.cam,
  170. "report": report, "format": 'XHTML',
  171. "prompt": 'true', "tracking": "", "params": {}}
  172. )
  173. r = self.session.post(self.webservice + 'v1/reports', data=soap, headers=headers)
  174. if r.status_code == 500:
  175. bs = BeautifulSoup(r.text, 'xml')
  176. report['error'] = bs.find_all('messageString')[0].string
  177. logging.error(report['error'])
  178. return report
  179. parts = decoder.MultipartDecoder.from_response(r).parts
  180. meta = {'required': {}, 'optional': {}}
  181. bs = BeautifulSoup(parts[1].content, 'lxml')
  182. for sv in bs.find_all('selectvalue'):
  183. k = sv['parameter']
  184. req = 'required' if sv['required'] == 'true' else 'optional'
  185. v = dict([(opt['usevalue'], opt['displayvalue']) for opt in sv.find_all('selectoption')])
  186. meta[req][k] = v
  187. for sv in bs.find_all('selectdate'):
  188. k = sv['parameter']
  189. req = 'required' if sv['required'] == 'true' else 'optional'
  190. v = dict([(opt['usevalue'], opt.get('displayvalue', '')) for opt in sv.find_all('selectoption')])
  191. meta[req][k] = v
  192. filename = self.cfg.xml_dir + f"/{report['path']}/{report['name']}.json"
  193. os.makedirs(os.path.dirname(filename), exist_ok=True)
  194. json.dump(meta, open(filename, 'w'), indent=2)
  195. report['meta'] = meta
  196. report['spec'] = parts[2].text
  197. path = report['path'].replace('Team Content/ReportOutput', '')
  198. report['filename'] = f"{self.cfg.cognos11.reportoutput_dir}/{path}/{report['name']}.pdf"
  199. report['format'] = 'PDF'
  200. if report['name'][-5:].lower() == '.xlsx':
  201. report['format'] = 'spreadsheetML'
  202. report['filename'] = report['filename'][:-4]
  203. report['params'] = list(re.findall(r'\[([^\]]+)\]', report['filename']))
  204. for i, p in enumerate(report['params']):
  205. report['filename'] = report['filename'].replace('[' + p + ']', '{' + str(i) + '}')
  206. return report
  207. def request_unstubbed(self, report_id):
  208. report = self.get_report(report_id)
  209. if 'spec' not in report:
  210. return ''
  211. payload = json.dumps({'reportspec_stubbed': report['spec'], 'storeid': report['id']})
  212. headers = {
  213. 'Content-Type': 'application/json; charset=UTF-8',
  214. 'X-XSRF-TOKEN': self.session.cookies.get('XSRF-TOKEN'),
  215. 'authenticityToken': self.cam,
  216. 'X-UseRsConsumerMode': 'true',
  217. 'cafContextId': self.caf
  218. }
  219. r = self.session.post(self.webservice + 'v1/reports/unstubreport', data=payload, headers=headers)
  220. unstubbed = json.loads(r.text)['reportspec_full']
  221. unstubbed = re.sub(r' iid="[^"]*"', '', unstubbed)
  222. bs = BeautifulSoup(unstubbed, 'xml')
  223. for xa in bs.find_all('XMLAttributes'):
  224. if (
  225. xa.find_all('XMLAttribute', {'name': 'RS_dataType'}) or
  226. xa.find_all('XMLAttribute', {'name': 'RS_CreateExtendedDataItems'}) or
  227. xa.find_all('XMLAttribute', {'name': 'RS_legacyDrillDown'})
  228. ):
  229. continue
  230. if xa.find_all('XMLAttribute', {'name': 'supportsDefaultDataFormatting'}):
  231. for xa2 in xa.find_all('XMLAttribute'):
  232. if xa2.attrs['name'] != 'supportsDefaultDataFormatting':
  233. xa2.decompose()
  234. continue
  235. xa.decompose()
  236. for cti in bs.find_all('crosstabIntersection'):
  237. if len(list(cti.children)) == 0:
  238. cti.decompose()
  239. unstubbed_report = str(bs)
  240. unstubbed_report = prettify_xml(unstubbed_report).replace("'", '"')
  241. return unstubbed_report
  242. def get_report_headers(self, report_id=None):
  243. res = {
  244. 'Content-Type': 'text/xml; charset=UTF-8',
  245. 'X-XSRF-TOKEN': self.headers['X-XSRF-TOKEN'],
  246. 'X-UseRsConsumerMode': 'true',
  247. 'SOAPAction': 'http://www.ibm.com/xmlns/prod/cognos/reportService/202004/'
  248. }
  249. if report_id is not None:
  250. res['X-RsCMStoreID'] = report_id
  251. return res
  252. def request_file(self, report_id, params, format='PDF'):
  253. report = self.get_report(report_id)
  254. headers = self.get_report_headers(report_id)
  255. soap = self.templates['get_report'].render(
  256. {"caf": self.caf, "cam": self.cam,
  257. "report": report, "format": format,
  258. "prompt": 'false', "tracking": "", "params": params}
  259. ).encode("utf-8")
  260. r = self.session.post(self.webservice + 'v1/reports', data=soap, headers=headers)
  261. bs = BeautifulSoup(r.text, 'xml')
  262. if r.status_code == 200:
  263. try:
  264. parts = decoder.MultipartDecoder.from_response(r).parts
  265. except decoder.NonMultipartContentTypeException:
  266. return 500, 'Timeout'
  267. return 200, parts[1].content
  268. error = bs.find_all('messageString')[0].string
  269. logging.debug(error)
  270. return r.status_code, error
  271. def create_report(self, folder_id, fullpath):
  272. # self.session.get(self.webservice + 'v1/reports/templates?path=%2Fcontent%2Ffolder%5B%40name%3D%27Templates%27%5D/
  273. # *[@objectClass=%27interactiveReport%27%20or%20@objectClass=%27report%27%20or%20@objectClass=%27reportTemplate%27]&maxResults=100&locale=de')
  274. # self.session.get(self.webservice + 'v1/reports/startupconfig?keys=supportedContentLocales,supportedCurrencies,
  275. # supportedFonts,metadataInformationURI,glossaryURI&locale=de')
  276. headers = self.get_report_headers()
  277. soap = self.templates['get_package'].render(
  278. {"caf": self.caf, "cam": self.cam}
  279. ).encode("utf-8")
  280. r = self.session.post(self.webservice + 'v1/reports', data=soap, headers=headers)
  281. open('package.xml', 'wb').write(r.content)
  282. search_path = self.request_search_path(folder_id)
  283. report_name = os.path.basename(fullpath)
  284. unstubbed = open(fullpath, 'rb').read()
  285. headers['SOAPAction'] = 'http://www.ibm.com/xmlns/prod/cognos/reportService/202004/.session'
  286. headers['Referer'] = 'http://localhost:9300/bi/pat/rsapp.htm'
  287. # headers['caf'] = self.caf
  288. soap = self.templates['create_report'].render(
  289. {"caf": self.caf, "cam": self.cam,
  290. "search_path": search_path,
  291. "report_name": report_name, "unstubbed": unstubbed}
  292. ).encode("utf-8")
  293. r = self.session.post(self.webservice + 'v1/reports', data=soap, headers=headers)
  294. open('request_create.xml', 'wb').write(r.request.body)
  295. print(r.status_code)
  296. print(r.text)
  297. def update_report(self, report, fullpath):
  298. search_path = self.request_search_path(report['id'])
  299. unstubbed = open(fullpath, 'r').read()
  300. headers = self.get_report_headers(report['id'])
  301. # headers['Referer'] = 'http://localhost:9300/bi/pat/rsapp.htm'
  302. headers['caf'] = self.caf
  303. soap = self.templates['update_report'].render(
  304. {"caf": self.caf, "cam": self.cam,
  305. "search_path": search_path, "unstubbed": unstubbed}
  306. ) # .encode("utf-8")
  307. r = self.session.post(self.webservice + 'v1/reports', data=soap, headers=headers)
  308. # open('request_update.xml', 'wb').write(r.request.body)
  309. print(r.status_code)
  310. print(r.text)
  311. def create_folder(self, parent_id, folder_name):
  312. data = json.dumps({"defaultName": folder_name, "type": "folder"})
  313. res = self.session.post(
  314. f"{self.webservice}v1/objects/{parent_id}/items",
  315. headers=self.headers,
  316. data=data
  317. )
  318. if res.status_code == 201:
  319. loc = res.headers.get('Location')
  320. folder_id = loc.split('/')[-1]
  321. self.folders.append({'id': folder_id, 'name': folder_name})
  322. return folder_id
  323. def request_search_path(self, id):
  324. res = self.session.get(f"{self.webservice}v1/objects/{id}?fields=searchPath", headers=self.headers)
  325. return res.json()['data'][0]['searchPath']
  326. if __name__ == '__main__':
  327. api = c11_api()
  328. api.login()
  329. folders = api.get_folders()
  330. filename = 'C:/GlobalCube/Tasks/gctools/logs/config/folders.json'
  331. json.dump(folders, open(filename, 'w'), indent=2)
  332. filename = 'C:/GlobalCube/Tasks/gctools/logs/config/reports.json'
  333. json.dump(api.reports, open(filename, 'w'), indent=2)
  334. filename = 'C:/GlobalCube/Tasks/gctools/logs/config/jobs.json'
  335. json.dump(api.jobs, open(filename, 'w'), indent=2)