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