c11_api.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  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. server_version = "202004"
  23. def __init__(self, cfg: config.Config):
  24. self.cfg = cfg
  25. self.webservice = cfg.cognos11.webservice
  26. self.templates_dir = cfg.tasks_dir + "/scripts/templates"
  27. self._env = jinja2.Environment(
  28. loader=jinja2.FileSystemLoader(self.templates_dir),
  29. autoescape=jinja2.select_autoescape(["html", "xml"]),
  30. )
  31. self.templates = {
  32. "get_report": self._env.get_template("get_report.xml"),
  33. "create_report": self._env.get_template("create_report.xml"),
  34. "update_report": self._env.get_template("update_report.xml"),
  35. "get_package": self._env.get_template("get_package.xml"),
  36. }
  37. @staticmethod
  38. def generate_token(message_base64):
  39. version = "V1".encode("utf-8")
  40. header_len = 4
  41. msg = base64.b64decode(message_base64)[1:]
  42. chunks = []
  43. while len(msg) >= header_len:
  44. chunk_len = int.from_bytes(msg[:header_len], byteorder="little")
  45. msg = msg[header_len:]
  46. chunks.append(msg[:chunk_len])
  47. msg = msg[chunk_len:]
  48. return base64.b64encode(version + chunks[-1]).decode("utf-8")
  49. def login(self):
  50. cred = self.cfg.cognos11.credentials
  51. credentials = {
  52. "parameters": [
  53. {"name": "h_CAM_action", "value": "logonAs"},
  54. {"name": "CAMNamespace", "value": cred.namespace},
  55. {"name": "CAMUsername", "value": cred.username},
  56. {"name": "CAMPassword", "value": cred.password},
  57. ]
  58. }
  59. self.session = requests.Session()
  60. r = self.session.get(self.webservice)
  61. self.headers = {
  62. "Content-Type": "application/json; charset=UTF-8",
  63. "X-XSRF-TOKEN": self.session.cookies.get("XSRF-TOKEN"),
  64. }
  65. r = self.session.post(
  66. self.webservice + "v1/login",
  67. data=json.dumps(credentials),
  68. headers=self.headers,
  69. )
  70. if r.ok:
  71. self.caf = r.json()["cafContextId"]
  72. self.cam = self.generate_token(r.cookies["usersessionid"])
  73. else:
  74. print("!! Error: Not connected to cognos server !!")
  75. print(f"Response: {r.status_code} {r.reason}")
  76. exit(1)
  77. self.server_version = self.get_server_version()
  78. return self
  79. def get_server_version(self):
  80. r = self.session.get(self.webservice + "pat/rsstartupblock_3.js")
  81. res = re.search(r"\=\"(\d{6})\";", r.text)
  82. if res:
  83. return res.group(1)
  84. return "202004"
  85. def get_folders(self):
  86. if len(self.folders) == 0:
  87. self.folders.append({"id": "_dot_public_folders", "name": "Team Content"})
  88. self.load_folder_list()
  89. self.save_config()
  90. return self.folders
  91. def save_config(self):
  92. os.makedirs(self.cfg.cognos11.config_dir, exist_ok=True)
  93. with open(self.cfg.cognos11.folders_file, "w") as fwh:
  94. json.dump(self.folders, fwh, indent=2)
  95. with open(self.cfg.cognos11.reports_file, "w") as fwh:
  96. json.dump(self.reports, fwh, indent=2)
  97. with open(self.cfg.cognos11.jobs_file, "w") as fwh:
  98. json.dump(self.jobs, fwh, indent=2)
  99. def load_config_from_files(self):
  100. with open(self.cfg.cognos11.folders_file, "r") as frh:
  101. self.folders = json.load(frh)
  102. with open(self.cfg.cognos11.reports_file, "r") as frh:
  103. self.reports = json.load(frh)
  104. with open(self.cfg.cognos11.jobs_file, "w") as frh:
  105. self.jobs = json.load(frh)
  106. def load_folder_list(self, folder_id="_dot_public_folders", prefix="Team Content"):
  107. fields = ",".join(
  108. [
  109. "id",
  110. "defaultName",
  111. "defaultDescription",
  112. "type",
  113. "modificationTime",
  114. "target.id",
  115. "target.searchPath",
  116. "target.defaultName",
  117. "base.id",
  118. "base.searchPath",
  119. "base.defaultName",
  120. "parameters",
  121. ]
  122. )
  123. res = self.session.get(
  124. f"{self.webservice}v1/objects/{folder_id}/items?fields={fields}",
  125. headers=self.headers,
  126. )
  127. folder_list = sorted(res.json()["data"], key=lambda x: x["defaultName"])
  128. for f in folder_list:
  129. if f["type"] == "folder":
  130. folder = {
  131. "id": f["id"],
  132. "name": prefix + "/" + f["defaultName"].replace("/", "-"),
  133. }
  134. self.folders.append(folder)
  135. self.load_folder_list(folder["id"], folder["name"])
  136. elif f["type"] in ("report", "reportView", "shortcut"):
  137. report = {
  138. "id": f["id"],
  139. "name": f["defaultName"].replace("/", "-"),
  140. "description": f["defaultDescription"],
  141. "modified": f["modificationTime"],
  142. "path": prefix,
  143. "type": f["type"],
  144. }
  145. if f["type"] == "shortcut":
  146. report["target_id"] = f["target"][0]["id"]
  147. if f["type"] == "reportView":
  148. report["target_id"] = "" if f["base"] is None else f["base"][0]["id"]
  149. report["parameters"] = {}
  150. if f["parameters"] is not None:
  151. params = [p for p in f["parameters"] if len(p["value"]) > 0]
  152. for p in params:
  153. report["parameters"][p["name"]] = dict([(v["use"], v["display"]) for v in p["value"]])
  154. self.reports.append(report)
  155. elif f["type"] == "jobDefinition":
  156. job = {"id": f["id"], "name": f["defaultName"], "path": prefix}
  157. job["details"] = self.get_job_details(job["id"])
  158. self.jobs.append(job)
  159. def get_report_details(self, object_id):
  160. fields = ",".join(
  161. [
  162. "defaultDescription",
  163. "options",
  164. "executionPrompt",
  165. "parameters",
  166. "module.defaultName",
  167. "module.ancestors",
  168. "allowNotification",
  169. "base.id",
  170. "base.searchPath",
  171. "base.defaultName",
  172. "base.defaultDescription",
  173. "base.ancestors",
  174. "base.metadataModelPackage",
  175. "base.module",
  176. ]
  177. )
  178. res = self.session.get(
  179. f"{self.webservice}v1/objects/{object_id}?fields={fields}",
  180. headers=self.headers,
  181. )
  182. res = res.json()["data"][0]
  183. res.pop("_meta", None)
  184. res.pop("id", None)
  185. res.pop("type", None)
  186. res.pop("defaultName", None)
  187. return res
  188. def get_job_details(self, object_id):
  189. fields = ",".join(
  190. [
  191. "userInterfaces",
  192. "disabled",
  193. "runInAdvancedViewer",
  194. "modificationTime",
  195. "canBurst",
  196. "defaultPortalAction",
  197. "base.defaultName",
  198. "tags",
  199. "target.searchPath",
  200. "target.disabled",
  201. "options",
  202. "base.options",
  203. ]
  204. )
  205. res = self.session.get(
  206. f"{self.webservice}v1/objects/{object_id}?fields={fields}",
  207. headers=self.headers,
  208. )
  209. job = res.json()["data"][0]
  210. job.pop("_meta", None)
  211. job.pop("id", None)
  212. job.pop("type", None)
  213. job.pop("defaultName", None)
  214. fields2 = ",".join(
  215. [
  216. r"id,displaySequence,stepObject{defaultName}",
  217. r"stepObject{id},stepObject{parameters}",
  218. r"stepObject{canBurst},options,parameters",
  219. ]
  220. )
  221. res = self.session.get(
  222. f"{self.webservice}v1/objects/{object_id}/items?types=jobStepDefinition&fields={fields2}",
  223. headers=self.headers,
  224. )
  225. steps = res.json()["data"]
  226. for s in steps:
  227. s.pop("_meta", None)
  228. if s["stepObject"] is not None:
  229. s["report_id"] = s["stepObject"][0]["id"]
  230. s.pop("stepObject", None)
  231. job["steps"] = steps
  232. return job
  233. def get_report(self, report_id):
  234. self.get_folders()
  235. report = [r for r in self.reports if r["id"] == report_id]
  236. if len(report) == 0:
  237. return None
  238. report = report[0]
  239. if "meta" not in report:
  240. report = self.get_report_specs(report)
  241. return report
  242. def get_reports_in_folder(self, folder, recursive=False, specs=False):
  243. self.get_folders()
  244. if recursive:
  245. res = [r for r in self.reports if r["path"].startswith(folder)]
  246. else:
  247. res = [r for r in self.reports if r["path"] == folder]
  248. if specs:
  249. return [self.get_report_specs(r) for r in res]
  250. return res
  251. def get_report_specs(self, report):
  252. report = self.get_report_filename(report)
  253. headers = {
  254. "Content-Type": "text/xml; charset=UTF-8",
  255. "X-XSRF-TOKEN": self.headers["X-XSRF-TOKEN"],
  256. "X-RsCMStoreID": report["id"],
  257. "X-UseRsConsumerMode": "true",
  258. "SOAPAction": f"http://www.ibm.com/xmlns/prod/cognos/reportService/{self.server_version}/",
  259. }
  260. soap = self.templates["get_report"].render(
  261. {
  262. "caf": self.caf,
  263. "cam": self.cam,
  264. "report": report,
  265. "format": "XHTML",
  266. "prompt": "true",
  267. "tracking": "",
  268. "params": {},
  269. }
  270. )
  271. r = self.session.post(self.webservice + "v1/reports", data=soap, headers=headers)
  272. if r.status_code == 500:
  273. bs = BeautifulSoup(r.text, "xml")
  274. report["error"] = bs.find_all("messageString")[0].string
  275. logging.error(f"{report['path']}/{report['name']}")
  276. logging.error(report["error"])
  277. return report
  278. parts = decoder.MultipartDecoder.from_response(r).parts
  279. meta = {"required": {}, "optional": {}}
  280. bs = BeautifulSoup(parts[0].content, "xml")
  281. result = bs.find("bus:result")
  282. details = result.find("bus:primaryRequest")
  283. params = details.find("bus:parameters")
  284. for item in params.find_all("item"):
  285. if item["xsi:type"] != "bus:parameterValue":
  286. continue
  287. k = item.find("bus:name").text
  288. # v = item.find("bus:value").find_all("item")
  289. v = {}
  290. for opt in item.find("bus:value").find_all("item"):
  291. if opt.find("bus:display") is not None:
  292. v[opt.find("bus:use").text] = opt.find("bus:display").text
  293. else:
  294. v[opt.find("bus:use").text] = opt.find("bus:use").text
  295. if len(v.items()) > 0:
  296. meta["required"][k] = v
  297. bs = BeautifulSoup(parts[1].content, "lxml")
  298. for sv in bs.find_all("selectvalue"):
  299. k = sv["parameter"]
  300. v = dict([(opt["usevalue"], opt.get("displayvalue", "")) for opt in sv.find_all("selectoption")])
  301. meta["optional"][k] = v
  302. for sv in bs.find_all("selectdate"):
  303. k = sv["parameter"]
  304. v = dict([(opt["usevalue"], opt.get("displayvalue", "")) for opt in sv.find_all("selectoption")])
  305. meta["optional"][k] = v
  306. filename = self.cfg.cognos11.config_dir + f"/params/{report['path']}/{report['name']}.json"
  307. os.makedirs(os.path.dirname(filename), exist_ok=True)
  308. json.dump(meta, open(filename, "w"), indent=2)
  309. report["cube"] = self.get_cube_name(meta)
  310. report["meta"] = meta
  311. report["spec"] = parts[2].text
  312. return report
  313. @staticmethod
  314. def get_cube_name(meta):
  315. for param in meta["optional"].values():
  316. for key in param.keys():
  317. if key.startswith("["):
  318. res = re.search(r"^\[([^\]]*)\]", key)
  319. return res.group(1)
  320. def get_report_filename(self, report):
  321. path = report["path"].replace("Team Content/ReportOutput", "")
  322. report["filename"] = f"{self.cfg.cognos11.reportoutput_dir}/{path}/{report['name']}.pdf"
  323. report["format"] = "PDF"
  324. if report["name"][-5:].lower() == ".xlsx":
  325. report["format"] = "spreadsheetML"
  326. report["filename"] = report["filename"][:-4]
  327. report["params"] = list(re.findall(r"\[([^\]]+)\]", report["filename"]))
  328. for i, p in enumerate(report["params"]):
  329. report["filename"] = report["filename"].replace("[" + p + "]", "{" + str(i) + "}")
  330. return report
  331. def request_unstubbed(self, report_id):
  332. report = self.get_report(report_id)
  333. if "spec" not in report:
  334. return ""
  335. payload = json.dumps({"reportspec_stubbed": report["spec"], "storeid": report["id"]})
  336. headers = {
  337. "Content-Type": "application/json; charset=UTF-8",
  338. "X-XSRF-TOKEN": self.session.cookies.get("XSRF-TOKEN"),
  339. "authenticityToken": self.cam,
  340. "X-UseRsConsumerMode": "true",
  341. "cafContextId": self.caf,
  342. }
  343. r = self.session.post(self.webservice + "v1/reports/unstubreport", data=payload, headers=headers)
  344. if r.status_code != 200:
  345. logging.error(f"{report['path']}/{report['name']}")
  346. logging.error(r.text)
  347. return
  348. unstubbed = json.loads(r.text)["reportspec_full"]
  349. unstubbed = re.sub(r' iid="[^"]*"', "", unstubbed)
  350. bs = BeautifulSoup(unstubbed, "xml")
  351. for xa in bs.find_all("XMLAttributes"):
  352. if (
  353. xa.find_all("XMLAttribute", {"name": "RS_dataType"})
  354. or xa.find_all("XMLAttribute", {"name": "RS_CreateExtendedDataItems"})
  355. or xa.find_all("XMLAttribute", {"name": "RS_legacyDrillDown"})
  356. ):
  357. continue
  358. if xa.find_all("XMLAttribute", {"name": "supportsDefaultDataFormatting"}):
  359. for xa2 in xa.find_all("XMLAttribute"):
  360. if xa2.attrs["name"] != "supportsDefaultDataFormatting":
  361. xa2.decompose()
  362. continue
  363. xa.decompose()
  364. for cti in bs.find_all("crosstabIntersection"):
  365. if len(list(cti.children)) == 0:
  366. cti.decompose()
  367. unstubbed_report = str(bs)
  368. unstubbed_report = prettify_xml(unstubbed_report).replace("'", """)
  369. return unstubbed_report
  370. def get_report_headers(self, report_id=None):
  371. res = {
  372. "Content-Type": "text/xml; charset=UTF-8",
  373. "X-XSRF-TOKEN": self.headers["X-XSRF-TOKEN"],
  374. "X-UseRsConsumerMode": "true",
  375. "SOAPAction": f"http://www.ibm.com/xmlns/prod/cognos/reportService/{self.server_version}/",
  376. }
  377. if report_id is not None:
  378. res["X-RsCMStoreID"] = report_id
  379. return res
  380. def request_file(self, report_id, params, format="PDF"):
  381. report = self.get_report(report_id)
  382. headers = self.get_report_headers(report_id)
  383. soap = (
  384. self.templates["get_report"]
  385. .render(
  386. {
  387. "caf": self.caf,
  388. "cam": self.cam,
  389. "report": report,
  390. "format": format,
  391. "prompt": "false",
  392. "tracking": "",
  393. "params": params,
  394. }
  395. )
  396. .encode("utf-8")
  397. )
  398. r = self.session.post(self.webservice + "v1/reports", data=soap, headers=headers)
  399. bs = BeautifulSoup(r.text, "xml")
  400. if r.status_code == 200:
  401. try:
  402. parts = decoder.MultipartDecoder.from_response(r).parts
  403. except decoder.NonMultipartContentTypeException:
  404. return 500, "Timeout"
  405. return 200, parts[1].content
  406. error = bs.find_all("messageString")[0].string
  407. logging.debug(error)
  408. return r.status_code, error
  409. def create_report(self, folder_id, fullpath):
  410. # self.session.get(self.webservice + 'v1/reports/templates?path=%2Fcontent%2Ffolder%5B%40name%3D%27Templates%27%5D/
  411. # *[@objectClass=%27interactiveReport%27%20or%20@objectClass=%27report%27%20or%20@objectClass=%27reportTemplate%27]&maxResults=100&locale=de')
  412. # self.session.get(self.webservice + 'v1/reports/startupconfig?keys=supportedContentLocales,supportedCurrencies,
  413. # supportedFonts,metadataInformationURI,glossaryURI&locale=de')
  414. headers = self.get_report_headers()
  415. soap = self.templates["get_package"].render({"caf": self.caf, "cam": self.cam}).encode("utf-8")
  416. r = self.session.post(self.webservice + "v1/reports", data=soap, headers=headers)
  417. open("package.xml", "wb").write(r.content)
  418. search_path = self.request_search_path(folder_id)
  419. report_name = os.path.basename(fullpath)
  420. unstubbed = open(fullpath, "rb").read()
  421. headers["SOAPAction"] = f"http://www.ibm.com/xmlns/prod/cognos/reportService/{self.server_version}/.session"
  422. headers["Referer"] = "http://localhost:9300/bi/pat/rsapp.htm"
  423. # headers['caf'] = self.caf
  424. soap = (
  425. self.templates["create_report"]
  426. .render(
  427. {
  428. "caf": self.caf,
  429. "cam": self.cam,
  430. "search_path": search_path,
  431. "report_name": report_name,
  432. "unstubbed": unstubbed,
  433. }
  434. )
  435. .encode("utf-8")
  436. )
  437. r = self.session.post(self.webservice + "v1/reports", data=soap, headers=headers)
  438. open("request_create.xml", "wb").write(r.request.body)
  439. print(r.status_code)
  440. print(r.text)
  441. def update_report(self, report, fullpath):
  442. search_path = self.request_search_path(report["id"])
  443. unstubbed = open(fullpath, "r").read()
  444. headers = self.get_report_headers(report["id"])
  445. # headers['Referer'] = 'http://localhost:9300/bi/pat/rsapp.htm'
  446. headers["caf"] = self.caf
  447. soap = self.templates["update_report"].render(
  448. {
  449. "caf": self.caf,
  450. "cam": self.cam,
  451. "search_path": search_path,
  452. "unstubbed": unstubbed,
  453. }
  454. ) # .encode("utf-8")
  455. r = self.session.post(self.webservice + "v1/reports", data=soap, headers=headers)
  456. # open('request_update.xml', 'wb').write(r.request.body)
  457. print(r.status_code)
  458. print(r.text)
  459. def create_folder(self, parent_id, folder_name):
  460. data = json.dumps({"defaultName": folder_name, "type": "folder"})
  461. res = self.session.post(
  462. f"{self.webservice}v1/objects/{parent_id}/items",
  463. headers=self.headers,
  464. data=data,
  465. )
  466. if res.status_code == 201:
  467. loc = res.headers.get("Location")
  468. folder_id = loc.split("/")[-1]
  469. self.folders.append({"id": folder_id, "name": folder_name})
  470. return folder_id
  471. def request_search_path(self, id):
  472. res = self.session.get(f"{self.webservice}v1/objects/{id}?fields=searchPath", headers=self.headers)
  473. return res.json()["data"][0]["searchPath"]
  474. if __name__ == "__main__":
  475. api = c11_api()
  476. api.login()
  477. folders = api.get_folders()
  478. # filename = "C:/GlobalCube/Tasks/gctools/logs/config/folders.json"
  479. # json.dump(folders, open(filename, "w"), indent=2)
  480. # filename = "C:/GlobalCube/Tasks/gctools/logs/config/reports.json"
  481. # json.dump(api.reports, open(filename, "w"), indent=2)
  482. # filename = "C:/GlobalCube/Tasks/gctools/logs/config/jobs.json"
  483. # json.dump(api.jobs, open(filename, "w"), indent=2)