ixbrl-viewer 1.4.29__py3-none-any.whl → 1.4.30__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ixbrl-viewer might be problematic. Click here for more details.
- iXBRLViewerPlugin/__init__.py +43 -22
- iXBRLViewerPlugin/_version.py +2 -2
- iXBRLViewerPlugin/iXBRLViewer.py +145 -125
- iXBRLViewerPlugin/plugin.py +7 -0
- iXBRLViewerPlugin/viewer/dist/ixbrlviewer.js +1 -1
- {ixbrl_viewer-1.4.29.dist-info → ixbrl_viewer-1.4.30.dist-info}/METADATA +1 -1
- {ixbrl_viewer-1.4.29.dist-info → ixbrl_viewer-1.4.30.dist-info}/RECORD +13 -12
- tests/unit_tests/iXBRLViewerPlugin/test_iXBRLViewer.py +24 -23
- {ixbrl_viewer-1.4.29.dist-info → ixbrl_viewer-1.4.30.dist-info}/LICENSE +0 -0
- {ixbrl_viewer-1.4.29.dist-info → ixbrl_viewer-1.4.30.dist-info}/NOTICE +0 -0
- {ixbrl_viewer-1.4.29.dist-info → ixbrl_viewer-1.4.30.dist-info}/WHEEL +0 -0
- {ixbrl_viewer-1.4.29.dist-info → ixbrl_viewer-1.4.30.dist-info}/entry_points.txt +0 -0
- {ixbrl_viewer-1.4.29.dist-info → ixbrl_viewer-1.4.30.dist-info}/top_level.txt +0 -0
iXBRLViewerPlugin/__init__.py
CHANGED
|
@@ -22,6 +22,9 @@ from .constants import CONFIG_COPY_SCRIPT, CONFIG_FEATURE_PREFIX, CONFIG_LAUNCH_
|
|
|
22
22
|
DEFAULT_JS_FILENAME, DEFAULT_VIEWER_PATH, ERROR_MESSAGE_CODE, \
|
|
23
23
|
EXCEPTION_MESSAGE_CODE, FEATURE_CONFIGS
|
|
24
24
|
from .iXBRLViewer import IXBRLViewerBuilder, IXBRLViewerBuilderError
|
|
25
|
+
from .plugin import IXBRLViewerPluginData
|
|
26
|
+
|
|
27
|
+
PLUGIN_NAME = 'ixbrl-viewer'
|
|
25
28
|
|
|
26
29
|
#
|
|
27
30
|
# GUI operation:
|
|
@@ -94,10 +97,6 @@ def iXBRLViewerCommandLineOptionExtender(parser, *args, **kwargs):
|
|
|
94
97
|
dest="zipViewerOutput",
|
|
95
98
|
help="Converts the viewer output into a self contained zip")
|
|
96
99
|
|
|
97
|
-
# Force "keepOpen" to true, so that all models are retained. Needed for
|
|
98
|
-
# multi-instance viewers.
|
|
99
|
-
parser.set_defaults(keepOpen = True)
|
|
100
|
-
|
|
101
100
|
featureGroup = OptionGroup(parser, "Viewer Features",
|
|
102
101
|
"See viewer README for information on enabling/disabling features.")
|
|
103
102
|
for featureConfig in FEATURE_CONFIGS:
|
|
@@ -105,13 +104,30 @@ def iXBRLViewerCommandLineOptionExtender(parser, *args, **kwargs):
|
|
|
105
104
|
featureGroup.add_option(arg, arg.lower(), action="store_true", default=False, help=featureConfig.description)
|
|
106
105
|
parser.add_option_group(featureGroup)
|
|
107
106
|
|
|
107
|
+
def pluginData(cntlr: Cntlr):
|
|
108
|
+
pluginData = cntlr.getPluginData(PLUGIN_NAME)
|
|
109
|
+
if pluginData is None:
|
|
110
|
+
pluginData = IXBRLViewerPluginData(PLUGIN_NAME)
|
|
111
|
+
cntlr.setPluginData(pluginData)
|
|
112
|
+
return pluginData
|
|
113
|
+
|
|
114
|
+
def resetPluginData(cntlr: Cntlr):
|
|
115
|
+
pluginData(cntlr).builder = None
|
|
116
|
+
|
|
117
|
+
def processModel(cntlr: Cntlr, modelXbrl: ModelXbrl):
|
|
118
|
+
try:
|
|
119
|
+
pluginData(cntlr).builder.processModel(modelXbrl)
|
|
120
|
+
except IXBRLViewerBuilderError as ex:
|
|
121
|
+
print(ex)
|
|
122
|
+
except Exception as ex:
|
|
123
|
+
tb = traceback.format_tb(sys.exc_info()[2])
|
|
124
|
+
cntlr.addToLog(f"Exception {ex} \nTraceback {tb}", messageCode=EXCEPTION_MESSAGE_CODE)
|
|
108
125
|
|
|
109
126
|
def generateViewer(
|
|
110
127
|
cntlr: Cntlr,
|
|
111
128
|
saveViewerDest: io.BytesIO | str | None,
|
|
112
129
|
viewerURL: str | None = None,
|
|
113
130
|
showValidationMessages: bool = False,
|
|
114
|
-
useStubViewer: bool = False,
|
|
115
131
|
zipViewerOutput: bool = False,
|
|
116
132
|
features: list[str] | None = None,
|
|
117
133
|
packageDownloadURL: str | None = None,
|
|
@@ -138,13 +154,10 @@ def generateViewer(
|
|
|
138
154
|
|
|
139
155
|
viewerURL = viewerURL or DEFAULT_VIEWER_PATH
|
|
140
156
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
cntlr.addToLog(f"No
|
|
145
|
-
return
|
|
146
|
-
if cntlr.modelManager.modelXbrl.modelDocument.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
|
|
147
|
-
cntlr.addToLog(f"No inline XBRL document loaded. {abortGenerationMsg}", messageCode=ERROR_MESSAGE_CODE)
|
|
157
|
+
bldr = pluginData(cntlr).builder
|
|
158
|
+
|
|
159
|
+
if bldr.reportCount == 0:
|
|
160
|
+
cntlr.addToLog(f"No inline XBRL documents loaded. {abortGenerationMsg}", messageCode=ERROR_MESSAGE_CODE)
|
|
148
161
|
return
|
|
149
162
|
|
|
150
163
|
copyScriptPath = None
|
|
@@ -171,11 +184,10 @@ def generateViewer(
|
|
|
171
184
|
return
|
|
172
185
|
|
|
173
186
|
try:
|
|
174
|
-
viewerBuilder = IXBRLViewerBuilder(cntlr.modelManager.loadedModelXbrls)
|
|
175
187
|
if features:
|
|
176
188
|
for feature in features:
|
|
177
|
-
|
|
178
|
-
iv =
|
|
189
|
+
bldr.enableFeature(feature)
|
|
190
|
+
iv = bldr.createViewer(scriptUrl=viewerURL, showValidations=showValidationMessages, packageDownloadURL=packageDownloadURL)
|
|
179
191
|
if iv is not None:
|
|
180
192
|
iv.save(saveViewerDest, zipOutput=zipViewerOutput, copyScriptPath=copyScriptPath)
|
|
181
193
|
except IXBRLViewerBuilderError as ex:
|
|
@@ -183,6 +195,7 @@ def generateViewer(
|
|
|
183
195
|
except Exception as ex:
|
|
184
196
|
tb = traceback.format_tb(sys.exc_info()[2])
|
|
185
197
|
cntlr.addToLog(f"Exception {ex} \nTraceback {tb}", messageCode=EXCEPTION_MESSAGE_CODE)
|
|
198
|
+
resetPluginData(cntlr)
|
|
186
199
|
|
|
187
200
|
|
|
188
201
|
def getFeaturesFromOptions(options: argparse.Namespace | OptionParser):
|
|
@@ -192,15 +205,19 @@ def getFeaturesFromOptions(options: argparse.Namespace | OptionParser):
|
|
|
192
205
|
if getattr(options, f'viewer_feature_{featureConfig.key}') or getattr(options, f'viewer_feature_{featureConfig.key.lower()}')
|
|
193
206
|
]
|
|
194
207
|
|
|
208
|
+
def iXBRLViewerCommandLineXbrlRun(cntlr, options, modelXbrl, *args, **kwargs):
|
|
209
|
+
pd = pluginData(cntlr)
|
|
210
|
+
if pd.builder is None:
|
|
211
|
+
pd.builder = IXBRLViewerBuilder(cntlr, useStubViewer = options.useStubViewer)
|
|
212
|
+
processModel(cntlr, modelXbrl)
|
|
195
213
|
|
|
196
|
-
def
|
|
214
|
+
def iXBRLViewerCommandLineFilingEnd(cntlr, options, *args, **kwargs):
|
|
197
215
|
generateViewer(
|
|
198
216
|
cntlr=cntlr,
|
|
199
217
|
saveViewerDest=options.saveViewerDest or kwargs.get("responseZipStream"),
|
|
200
218
|
viewerURL=options.viewerURL,
|
|
201
219
|
copyScript=not options.viewerNoCopyScript,
|
|
202
220
|
showValidationMessages=options.validationMessages,
|
|
203
|
-
useStubViewer=options.useStubViewer,
|
|
204
221
|
zipViewerOutput=options.zipViewerOutput,
|
|
205
222
|
features=getFeaturesFromOptions(options),
|
|
206
223
|
packageDownloadURL=options.packageDownloadURL,
|
|
@@ -259,9 +276,11 @@ def commandLineOptionExtender(*args, **kwargs):
|
|
|
259
276
|
iXBRLViewerCommandLineOptionExtender(*args, **kwargs)
|
|
260
277
|
|
|
261
278
|
|
|
262
|
-
def commandLineRun(*args, **kwargs):
|
|
263
|
-
iXBRLViewerCommandLineXbrlRun(*args, **kwargs)
|
|
279
|
+
def commandLineRun(cntlr, options, modelXbrl, *args, **kwargs):
|
|
280
|
+
iXBRLViewerCommandLineXbrlRun(cntlr, options, modelXbrl, *args, **kwargs)
|
|
264
281
|
|
|
282
|
+
def commandLineFilingEnd(*args, **kwargs):
|
|
283
|
+
iXBRLViewerCommandLineFilingEnd(*args, **kwargs)
|
|
265
284
|
|
|
266
285
|
class iXBRLViewerLocalViewer(LocalViewer):
|
|
267
286
|
# plugin-specific local file handler
|
|
@@ -300,12 +319,13 @@ def guiRun(cntlr, modelXbrl, attach, *args, **kwargs):
|
|
|
300
319
|
for c in FEATURE_CONFIGS
|
|
301
320
|
if cntlr.config.setdefault(f'{CONFIG_FEATURE_PREFIX}{c.key}', False)
|
|
302
321
|
]
|
|
322
|
+
pluginData(cntlr).builder = IXBRLViewerBuilder(cntlr, useStubViewer = True)
|
|
323
|
+
processModel(cntlr, modelXbrl)
|
|
303
324
|
generateViewer(
|
|
304
325
|
cntlr=cntlr,
|
|
305
326
|
saveViewerDest=tempViewer.name,
|
|
306
327
|
viewerURL=cntlr.config.get(CONFIG_SCRIPT_URL),
|
|
307
328
|
copyScript=cntlr.config.get(CONFIG_COPY_SCRIPT, DEFAULT_COPY_SCRIPT),
|
|
308
|
-
useStubViewer=True,
|
|
309
329
|
features=features,
|
|
310
330
|
)
|
|
311
331
|
if Path(tempViewer.name, viewer_file_name).exists():
|
|
@@ -325,7 +345,7 @@ def load_plugin_url():
|
|
|
325
345
|
|
|
326
346
|
|
|
327
347
|
__pluginInfo__ = {
|
|
328
|
-
'name':
|
|
348
|
+
'name': PLUGIN_NAME,
|
|
329
349
|
'aliases': [
|
|
330
350
|
'iXBRLViewerPlugin',
|
|
331
351
|
],
|
|
@@ -335,7 +355,8 @@ __pluginInfo__ = {
|
|
|
335
355
|
'author': 'Paul Warren',
|
|
336
356
|
'copyright': 'Copyright :: Workiva Inc. :: 2019',
|
|
337
357
|
'CntlrCmdLine.Options': commandLineOptionExtender,
|
|
338
|
-
'CntlrCmdLine.
|
|
358
|
+
'CntlrCmdLine.Xbrl.Run': commandLineRun,
|
|
359
|
+
'CntlrCmdLine.Filing.End': commandLineFilingEnd,
|
|
339
360
|
'CntlrWinMain.Menu.Tools': toolsMenuExtender,
|
|
340
361
|
'CntlrWinMain.Xbrl.Loaded': guiRun,
|
|
341
362
|
}
|
iXBRLViewerPlugin/_version.py
CHANGED
iXBRLViewerPlugin/iXBRLViewer.py
CHANGED
|
@@ -26,7 +26,6 @@ from lxml import etree
|
|
|
26
26
|
from .constants import DEFAULT_JS_FILENAME, DEFAULT_OUTPUT_NAME, ERROR_MESSAGE_CODE, FEATURE_CONFIGS, INFO_MESSAGE_CODE
|
|
27
27
|
from .xhtmlserialize import XHTMLSerializer
|
|
28
28
|
|
|
29
|
-
|
|
30
29
|
UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE = 'unrecognizedLinkbase'
|
|
31
30
|
LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE = {
|
|
32
31
|
XbrlConst.qnLinkCalculationLink: 'calcLinkbase',
|
|
@@ -82,18 +81,38 @@ class IXBRLViewerBuilderError(Exception):
|
|
|
82
81
|
|
|
83
82
|
class IXBRLViewerBuilder:
|
|
84
83
|
|
|
85
|
-
def __init__(self,
|
|
84
|
+
def __init__(self,
|
|
85
|
+
cntlr: Cntlr,
|
|
86
|
+
basenameSuffix: str = '',
|
|
87
|
+
useStubViewer: bool = False,
|
|
88
|
+
):
|
|
86
89
|
self.nsmap = NamespaceMap()
|
|
87
90
|
self.roleMap = NamespaceMap()
|
|
88
|
-
self.reports = reports
|
|
89
|
-
# Arbitrary ModelXbrl used for logging
|
|
90
|
-
self.logger_model = reports[0]
|
|
91
91
|
self.taxonomyData = {
|
|
92
92
|
"sourceReports": [],
|
|
93
93
|
"features": [],
|
|
94
94
|
}
|
|
95
95
|
self.basenameSuffix = basenameSuffix
|
|
96
96
|
self.currentTargetReport = None
|
|
97
|
+
self.useStubViewer = useStubViewer
|
|
98
|
+
self.cntlr = cntlr
|
|
99
|
+
|
|
100
|
+
self.idGen = 0
|
|
101
|
+
self.roleMap.getPrefix(XbrlConst.standardLabel, "std")
|
|
102
|
+
self.roleMap.getPrefix(XbrlConst.documentationLabel, "doc")
|
|
103
|
+
self.roleMap.getPrefix(XbrlConst.summationItem, "calc")
|
|
104
|
+
self.roleMap.getPrefix(XbrlConst.summationItem, "calc11")
|
|
105
|
+
self.roleMap.getPrefix(XbrlConst.parentChild, "pres")
|
|
106
|
+
self.roleMap.getPrefix(XbrlConst.dimensionDefault, "d-d")
|
|
107
|
+
self.roleMap.getPrefix(WIDER_NARROWER_ARCROLE, "w-n")
|
|
108
|
+
|
|
109
|
+
self.sourceReportsByFiles = dict()
|
|
110
|
+
self.iv = iXBRLViewer(cntlr)
|
|
111
|
+
if self.useStubViewer:
|
|
112
|
+
self.iv.addFile(iXBRLViewerFile(DEFAULT_OUTPUT_NAME, self.getStubDocument()))
|
|
113
|
+
|
|
114
|
+
self.fromSingleZIP = None
|
|
115
|
+
self.reportCount = 0
|
|
97
116
|
|
|
98
117
|
def enableFeature(self, featureName: str):
|
|
99
118
|
if featureName in self.taxonomyData["features"]:
|
|
@@ -210,7 +229,7 @@ class IXBRLViewerBuilder:
|
|
|
210
229
|
return rels
|
|
211
230
|
|
|
212
231
|
def validationErrors(self):
|
|
213
|
-
logHandler = self.
|
|
232
|
+
logHandler = self.cntlr.logHandler
|
|
214
233
|
if getattr(logHandler, "logRecordBuffer") is None:
|
|
215
234
|
raise IXBRLViewerBuilderError("Logging is not configured to use a buffer. Unable to retrieve validation messages")
|
|
216
235
|
|
|
@@ -326,14 +345,13 @@ class IXBRLViewerBuilder:
|
|
|
326
345
|
return numeratorsString
|
|
327
346
|
|
|
328
347
|
def addViewerData(self, viewerFile, scriptUrl):
|
|
329
|
-
viewerFile.xmlDocument = deepcopy(viewerFile.xmlDocument)
|
|
330
348
|
taxonomyDataJSON = self.escapeJSONForScriptTag(json.dumps(self.taxonomyData, indent=1, allow_nan=False))
|
|
331
349
|
|
|
332
350
|
for child in viewerFile.xmlDocument.getroot():
|
|
333
351
|
if child.tag == '{http://www.w3.org/1999/xhtml}body':
|
|
334
352
|
for body_child in child:
|
|
335
353
|
if body_child.tag == '{http://www.w3.org/1999/xhtml}script' and body_child.get('type','') == 'application/x.ixbrl-viewer+json':
|
|
336
|
-
self.
|
|
354
|
+
self.cntlr.addToLog("File already contains iXBRL viewer", messageCode="error")
|
|
337
355
|
return False
|
|
338
356
|
|
|
339
357
|
child.append(etree.Comment("BEGIN IXBRL VIEWER EXTENSIONS"))
|
|
@@ -375,111 +393,103 @@ class IXBRLViewerBuilder:
|
|
|
375
393
|
self.taxonomyData["sourceReports"].append(sourceReport)
|
|
376
394
|
return sourceReport
|
|
377
395
|
|
|
396
|
+
def processModel(
|
|
397
|
+
self,
|
|
398
|
+
report: ModelXbrl
|
|
399
|
+
):
|
|
400
|
+
|
|
401
|
+
self.footnoteRelationshipSet = ModelRelationshipSet(report, "XBRL-footnotes")
|
|
402
|
+
self.currentTargetReport = self.newTargetReport(getattr(report, "ixdsTarget", None))
|
|
403
|
+
softwareCredits = set()
|
|
404
|
+
for document in report.urlDocs.values():
|
|
405
|
+
if document.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
|
|
406
|
+
continue
|
|
407
|
+
matches = document.creationSoftwareMatches(document.creationSoftwareComment)
|
|
408
|
+
softwareCredits.update(matches)
|
|
409
|
+
if softwareCredits:
|
|
410
|
+
self.currentTargetReport["softwareCredits"] = list(softwareCredits)
|
|
411
|
+
for f in report.facts:
|
|
412
|
+
self.addFact(report, f)
|
|
413
|
+
self.currentTargetReport["rels"] = self.getRelationships(report)
|
|
414
|
+
|
|
415
|
+
docSetFiles = None
|
|
416
|
+
self.reportCount += 1
|
|
417
|
+
report.info(INFO_MESSAGE_CODE, "Creating iXBRL viewer (%d) [%s]" % (self.reportCount, self.currentTargetReport["target"]))
|
|
418
|
+
if report.modelDocument.type == Type.INLINEXBRLDOCUMENTSET:
|
|
419
|
+
# Sort by object index to preserve order in which files were specified.
|
|
420
|
+
xmlDocsByFilename = {
|
|
421
|
+
os.path.basename(self.outputFilename(doc.filepath)): doc.xmlDocument
|
|
422
|
+
for doc in sorted(report.modelDocument.referencesDocument.keys(), key=lambda x: x.objectIndex)
|
|
423
|
+
if doc.type == Type.INLINEXBRL
|
|
424
|
+
}
|
|
425
|
+
docSetFiles = list(xmlDocsByFilename.keys())
|
|
426
|
+
|
|
427
|
+
for filename, docSetXMLDoc in xmlDocsByFilename.items():
|
|
428
|
+
self.iv.addFile(iXBRLViewerFile(filename, docSetXMLDoc))
|
|
429
|
+
|
|
430
|
+
elif self.useStubViewer:
|
|
431
|
+
filename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
432
|
+
docSetFiles = [ filename ]
|
|
433
|
+
self.iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
434
|
+
|
|
435
|
+
else:
|
|
436
|
+
srcFilename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
437
|
+
docSetFiles = [ srcFilename ]
|
|
438
|
+
filename = srcFilename
|
|
439
|
+
self.iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
440
|
+
|
|
441
|
+
docSetKey = frozenset(docSetFiles)
|
|
442
|
+
sourceReport = self.sourceReportsByFiles.get(docSetKey)
|
|
443
|
+
if sourceReport is None:
|
|
444
|
+
sourceReport = self.addSourceReport()
|
|
445
|
+
self.sourceReportsByFiles[docSetKey] = sourceReport
|
|
446
|
+
sourceReport["docSetFiles"] = list(urllib.parse.quote(f) for f in docSetFiles)
|
|
447
|
+
|
|
448
|
+
sourceReport["targetReports"].append(self.currentTargetReport)
|
|
449
|
+
|
|
450
|
+
localDocs = defaultdict(set)
|
|
451
|
+
for path, doc in report.urlDocs.items():
|
|
452
|
+
if isHttpUrl(path) or doc.type == Type.INLINEXBRLDOCUMENTSET:
|
|
453
|
+
continue
|
|
454
|
+
if doc.type == Type.INLINEXBRL:
|
|
455
|
+
localDocs[doc.basename].add('inline')
|
|
456
|
+
elif doc.type == Type.SCHEMA:
|
|
457
|
+
localDocs[doc.basename].add('schema')
|
|
458
|
+
elif doc.type == Type.LINKBASE:
|
|
459
|
+
linkbaseIdentifed = False
|
|
460
|
+
for child in doc.xmlRootElement.iterchildren():
|
|
461
|
+
linkbaseLocalDocumentsKey = LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE.get(child.qname)
|
|
462
|
+
if linkbaseLocalDocumentsKey is not None:
|
|
463
|
+
localDocs[doc.basename].add(linkbaseLocalDocumentsKey)
|
|
464
|
+
linkbaseIdentifed = True
|
|
465
|
+
if not linkbaseIdentifed:
|
|
466
|
+
localDocs[doc.basename].add(UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE)
|
|
467
|
+
self.currentTargetReport["localDocs"] = {
|
|
468
|
+
localDoc: sorted(docTypes)
|
|
469
|
+
for localDoc, docTypes in localDocs.items()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# If we only process a single ZIP, add a download link to it as the
|
|
473
|
+
# "filing documents" on the viewer menu.
|
|
474
|
+
if self.fromSingleZIP is None:
|
|
475
|
+
self.fromSingleZIP = report.modelDocument.filepath.endswith(".zip")
|
|
476
|
+
if self.fromSingleZIP:
|
|
477
|
+
self.filingDocZipPath = os.path.dirname(report.modelDocument.filepath)
|
|
478
|
+
else:
|
|
479
|
+
self.fromSingleZIP = False
|
|
480
|
+
|
|
378
481
|
def createViewer(
|
|
379
482
|
self,
|
|
380
483
|
scriptUrl: str = DEFAULT_JS_FILENAME,
|
|
381
|
-
useStubViewer: bool = False,
|
|
382
484
|
showValidations: bool = True,
|
|
383
485
|
packageDownloadURL: str | None = None,
|
|
384
486
|
) -> iXBRLViewer | None:
|
|
385
487
|
"""
|
|
386
488
|
Create an iXBRL file with XBRL data as a JSON blob, and script tags added.
|
|
387
489
|
:param scriptUrl: The `src` value of the script tag that loads the viewer script.
|
|
388
|
-
:param useStubViewer: True if stub document should be included in output.
|
|
389
490
|
:param showValidations: True if validation errors should be included in output taxonomy data.
|
|
390
491
|
:return: An iXBRLViewer instance that is ready to be saved.
|
|
391
492
|
"""
|
|
392
|
-
# This "dts" is only used for logging
|
|
393
|
-
iv = iXBRLViewer(self.reports[0])
|
|
394
|
-
self.idGen = 0
|
|
395
|
-
self.roleMap.getPrefix(XbrlConst.standardLabel, "std")
|
|
396
|
-
self.roleMap.getPrefix(XbrlConst.documentationLabel, "doc")
|
|
397
|
-
self.roleMap.getPrefix(XbrlConst.summationItem, "calc")
|
|
398
|
-
self.roleMap.getPrefix(XbrlConst.summationItem11, "calc11")
|
|
399
|
-
self.roleMap.getPrefix(XbrlConst.parentChild, "pres")
|
|
400
|
-
self.roleMap.getPrefix(XbrlConst.dimensionDefault, "d-d")
|
|
401
|
-
self.roleMap.getPrefix(WIDER_NARROWER_ARCROLE, "w-n")
|
|
402
|
-
|
|
403
|
-
sourceReportsByFiles = dict()
|
|
404
|
-
|
|
405
|
-
if useStubViewer:
|
|
406
|
-
iv.addFile(iXBRLViewerFile(DEFAULT_OUTPUT_NAME, self.getStubDocument()))
|
|
407
|
-
|
|
408
|
-
for n, report in enumerate(self.reports):
|
|
409
|
-
self.footnoteRelationshipSet = ModelRelationshipSet(report, "XBRL-footnotes")
|
|
410
|
-
self.currentTargetReport = self.newTargetReport(getattr(report, "ixdsTarget", None))
|
|
411
|
-
softwareCredits = set()
|
|
412
|
-
for document in report.urlDocs.values():
|
|
413
|
-
if document.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
|
|
414
|
-
continue
|
|
415
|
-
matches = document.creationSoftwareMatches(document.creationSoftwareComment)
|
|
416
|
-
softwareCredits.update(matches)
|
|
417
|
-
if softwareCredits:
|
|
418
|
-
self.currentTargetReport["softwareCredits"] = list(softwareCredits)
|
|
419
|
-
for f in report.facts:
|
|
420
|
-
self.addFact(report, f)
|
|
421
|
-
self.currentTargetReport["rels"] = self.getRelationships(report)
|
|
422
|
-
|
|
423
|
-
docSetFiles = None
|
|
424
|
-
report.info(INFO_MESSAGE_CODE, "Creating iXBRL viewer (%d of %d)" % (n+1, len(self.reports)))
|
|
425
|
-
if report.modelDocument.type == Type.INLINEXBRLDOCUMENTSET:
|
|
426
|
-
# Sort by object index to preserve order in which files were specified.
|
|
427
|
-
xmlDocsByFilename = {
|
|
428
|
-
os.path.basename(self.outputFilename(doc.filepath)): doc.xmlDocument
|
|
429
|
-
for doc in sorted(report.modelDocument.referencesDocument.keys(), key=lambda x: x.objectIndex)
|
|
430
|
-
if doc.type == Type.INLINEXBRL
|
|
431
|
-
}
|
|
432
|
-
docSetFiles = list(xmlDocsByFilename.keys())
|
|
433
|
-
|
|
434
|
-
for filename, docSetXMLDoc in xmlDocsByFilename.items():
|
|
435
|
-
iv.addFile(iXBRLViewerFile(filename, docSetXMLDoc))
|
|
436
|
-
|
|
437
|
-
elif useStubViewer:
|
|
438
|
-
filename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
439
|
-
docSetFiles = [ filename ]
|
|
440
|
-
iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
441
|
-
|
|
442
|
-
else:
|
|
443
|
-
srcFilename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
|
|
444
|
-
docSetFiles = [ srcFilename ]
|
|
445
|
-
if len(self.reports) == 1:
|
|
446
|
-
# If there is only a single report, call the output file "xbrlviewer.html"
|
|
447
|
-
filename = "xbrlviewer.html"
|
|
448
|
-
else:
|
|
449
|
-
# Otherwise, preserve filenames
|
|
450
|
-
filename = srcFilename
|
|
451
|
-
iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
|
|
452
|
-
|
|
453
|
-
docSetKey = frozenset(docSetFiles)
|
|
454
|
-
sourceReport = sourceReportsByFiles.get(docSetKey)
|
|
455
|
-
if sourceReport is None:
|
|
456
|
-
sourceReport = self.addSourceReport()
|
|
457
|
-
sourceReportsByFiles[docSetKey] = sourceReport
|
|
458
|
-
sourceReport["docSetFiles"] = list(urllib.parse.quote(f) for f in docSetFiles)
|
|
459
|
-
|
|
460
|
-
sourceReport["targetReports"].append(self.currentTargetReport)
|
|
461
|
-
|
|
462
|
-
localDocs = defaultdict(set)
|
|
463
|
-
for path, doc in report.urlDocs.items():
|
|
464
|
-
if isHttpUrl(path) or doc.type == Type.INLINEXBRLDOCUMENTSET:
|
|
465
|
-
continue
|
|
466
|
-
if doc.type == Type.INLINEXBRL:
|
|
467
|
-
localDocs[doc.basename].add('inline')
|
|
468
|
-
elif doc.type == Type.SCHEMA:
|
|
469
|
-
localDocs[doc.basename].add('schema')
|
|
470
|
-
elif doc.type == Type.LINKBASE:
|
|
471
|
-
linkbaseIdentifed = False
|
|
472
|
-
for child in doc.xmlRootElement.iterchildren():
|
|
473
|
-
linkbaseLocalDocumentsKey = LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE.get(child.qname)
|
|
474
|
-
if linkbaseLocalDocumentsKey is not None:
|
|
475
|
-
localDocs[doc.basename].add(linkbaseLocalDocumentsKey)
|
|
476
|
-
linkbaseIdentifed = True
|
|
477
|
-
if not linkbaseIdentifed:
|
|
478
|
-
localDocs[doc.basename].add(UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE)
|
|
479
|
-
self.currentTargetReport["localDocs"] = {
|
|
480
|
-
localDoc: sorted(docTypes)
|
|
481
|
-
for localDoc, docTypes in localDocs.items()
|
|
482
|
-
}
|
|
483
493
|
|
|
484
494
|
self.taxonomyData["prefixes"] = self.nsmap.prefixmap
|
|
485
495
|
self.taxonomyData["roles"] = self.roleMap.prefixmap
|
|
@@ -489,32 +499,42 @@ class IXBRLViewerBuilder:
|
|
|
489
499
|
|
|
490
500
|
if packageDownloadURL is not None:
|
|
491
501
|
self.taxonomyData["filingDocuments"] = packageDownloadURL
|
|
492
|
-
elif
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
iv.addFilingDoc(filingDocZipPath)
|
|
502
|
+
elif self.fromSingleZIP:
|
|
503
|
+
filingDocZipName = os.path.basename(self.filingDocZipPath)
|
|
504
|
+
self.iv.addFilingDoc(self.filingDocZipPath)
|
|
496
505
|
self.taxonomyData["filingDocuments"] = filingDocZipName
|
|
497
506
|
|
|
498
|
-
if not self.addViewerData(iv.files[0], scriptUrl):
|
|
507
|
+
if not self.addViewerData(self.iv.files[0], scriptUrl):
|
|
499
508
|
return None
|
|
500
509
|
|
|
501
|
-
|
|
510
|
+
if len(self.iv.files) == 1:
|
|
511
|
+
# If there is only a single report, call the output file "xbrlviewer.html"
|
|
512
|
+
# We should probably preserve the source file extension here.
|
|
513
|
+
self.iv.files[0].filename = 'xbrlviewer.html'
|
|
514
|
+
|
|
515
|
+
return self.iv
|
|
502
516
|
|
|
503
517
|
|
|
504
518
|
class iXBRLViewerFile:
|
|
505
519
|
|
|
506
520
|
def __init__(self, filename, xmlDocument):
|
|
507
521
|
self.filename = filename
|
|
508
|
-
self.xmlDocument = xmlDocument
|
|
522
|
+
self.xmlDocument = deepcopy(xmlDocument)
|
|
523
|
+
# deepcopy does not retain the Python proxies, so iterating the node
|
|
524
|
+
# tree during serialization will create new ones. However, the original
|
|
525
|
+
# ModelObjectFactory is still referenced, and that references a
|
|
526
|
+
# ModelXbrl that will potentially be closed by the time we serialize.
|
|
527
|
+
# Serialization only requires standard XML features, so the default
|
|
528
|
+
# lxml.etree classes (and thus lookup) are fine.
|
|
529
|
+
self.xmlDocument.parser.set_element_class_lookup(etree.ElementDefaultClassLookup())
|
|
509
530
|
|
|
510
531
|
|
|
511
532
|
class iXBRLViewer:
|
|
512
533
|
|
|
513
|
-
def __init__(self,
|
|
534
|
+
def __init__(self, cntlr: Cntlr):
|
|
514
535
|
self.files = []
|
|
515
536
|
self.filingDocuments = None
|
|
516
|
-
|
|
517
|
-
self.logger_model = logger_model
|
|
537
|
+
self.cntlr = cntlr
|
|
518
538
|
self.filenames = set()
|
|
519
539
|
|
|
520
540
|
def addFile(self, ivf):
|
|
@@ -544,15 +564,15 @@ class iXBRLViewer:
|
|
|
544
564
|
fileMode = 'w'
|
|
545
565
|
elif destination.endswith(os.sep):
|
|
546
566
|
# Looks like a directory, but isn't one
|
|
547
|
-
self.
|
|
567
|
+
self.cntlr.addToLog("Directory %s does not exist" % destination, messageCode=ERROR_MESSAGE_CODE)
|
|
548
568
|
return
|
|
549
569
|
elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
|
|
550
570
|
# Directory part of filename doesn't exist
|
|
551
|
-
self.
|
|
571
|
+
self.cntlr.addToLog("Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)), messageCode=ERROR_MESSAGE_CODE)
|
|
552
572
|
return
|
|
553
573
|
elif not destination.endswith('.zip'):
|
|
554
574
|
# File extension isn't a zip
|
|
555
|
-
self.
|
|
575
|
+
self.cntlr.addToLog("File extension %s is not a zip" % os.path.splitext(destination)[0], messageCode=ERROR_MESSAGE_CODE)
|
|
556
576
|
return
|
|
557
577
|
else:
|
|
558
578
|
file = destination
|
|
@@ -560,49 +580,49 @@ class iXBRLViewer:
|
|
|
560
580
|
|
|
561
581
|
with zipfile.ZipFile(file, fileMode, zipfile.ZIP_DEFLATED, True) as zout:
|
|
562
582
|
for f in self.files:
|
|
563
|
-
self.
|
|
583
|
+
self.cntlr.addToLog("Saving in output zip %s" % f.filename, messageCode=INFO_MESSAGE_CODE)
|
|
564
584
|
with zout.open(f.filename, "w") as fout:
|
|
565
585
|
writer = XHTMLSerializer(fout)
|
|
566
586
|
writer.serialize(f.xmlDocument)
|
|
567
587
|
if self.filingDocuments:
|
|
568
588
|
filename = os.path.basename(self.filingDocuments)
|
|
569
|
-
self.
|
|
589
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
570
590
|
zout.write(self.filingDocuments, filename)
|
|
571
591
|
if copyScriptPath is not None:
|
|
572
|
-
self.
|
|
592
|
+
self.cntlr.addToLog(f"Writing script from {copyScriptPath}", messageCode=INFO_MESSAGE_CODE)
|
|
573
593
|
zout.write(copyScriptPath, copyScriptPath.name)
|
|
574
594
|
elif os.path.isdir(destination):
|
|
575
595
|
# If output is a directory, write each file in the doc set to that
|
|
576
596
|
# directory using its existing filename
|
|
577
597
|
for f in self.files:
|
|
578
598
|
filename = os.path.join(destination, f.filename)
|
|
579
|
-
self.
|
|
599
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
580
600
|
with open(filename, "wb") as fout:
|
|
581
601
|
writer = XHTMLSerializer(fout)
|
|
582
602
|
writer.serialize(f.xmlDocument)
|
|
583
603
|
if self.filingDocuments:
|
|
584
604
|
filename = os.path.basename(self.filingDocuments)
|
|
585
|
-
self.
|
|
605
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
586
606
|
shutil.copy2(self.filingDocuments, os.path.join(destination, filename))
|
|
587
607
|
if copyScriptPath is not None:
|
|
588
608
|
self._copyScript(Path(destination), copyScriptPath)
|
|
589
609
|
else:
|
|
590
610
|
if len(self.files) > 1:
|
|
591
|
-
self.
|
|
611
|
+
self.cntlr.addToLog("More than one file in input, but output is not a directory", messageCode=ERROR_MESSAGE_CODE)
|
|
592
612
|
elif destination.endswith(os.sep):
|
|
593
613
|
# Looks like a directory, but isn't one
|
|
594
|
-
self.
|
|
614
|
+
self.cntlr.addToLog("Directory %s does not exist" % destination, messageCode=ERROR_MESSAGE_CODE)
|
|
595
615
|
elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
|
|
596
616
|
# Directory part of filename doesn't exist
|
|
597
|
-
self.
|
|
617
|
+
self.cntlr.addToLog("Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)), messageCode=ERROR_MESSAGE_CODE)
|
|
598
618
|
else:
|
|
599
|
-
self.
|
|
619
|
+
self.cntlr.addToLog("Writing %s" % destination, messageCode=INFO_MESSAGE_CODE)
|
|
600
620
|
with open(destination, "wb") as fout:
|
|
601
621
|
writer = XHTMLSerializer(fout)
|
|
602
622
|
writer.serialize(self.files[0].xmlDocument)
|
|
603
623
|
if self.filingDocuments:
|
|
604
624
|
filename = os.path.basename(self.filingDocuments)
|
|
605
|
-
self.
|
|
625
|
+
self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
|
|
606
626
|
shutil.copy2(self.filingDocuments, os.path.join(os.path.dirname(destination), filename))
|
|
607
627
|
if copyScriptPath is not None:
|
|
608
628
|
outDirectory = Path(destination).parent
|
|
@@ -611,5 +631,5 @@ class iXBRLViewer:
|
|
|
611
631
|
def _copyScript(self, destDirectory: Path, scriptPath: Path):
|
|
612
632
|
scriptDest = destDirectory / scriptPath.name
|
|
613
633
|
if scriptPath != scriptDest:
|
|
614
|
-
self.
|
|
634
|
+
self.cntlr.addToLog(f"Copying script from {scriptPath} to {scriptDest}.", messageCode=INFO_MESSAGE_CODE)
|
|
615
635
|
shutil.copy2(scriptPath, scriptDest)
|