arelle-release 2.37.28__py3-none-any.whl → 2.37.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 arelle-release might be problematic. Click here for more details.

Files changed (62) hide show
  1. arelle/Locale.py +3 -1
  2. arelle/ModelTestcaseObject.py +9 -3
  3. arelle/Validate.py +29 -14
  4. arelle/WebCache.py +10 -5
  5. arelle/_version.py +2 -2
  6. arelle/plugin/validate/EDINET/DisclosureSystems.py +1 -0
  7. arelle/plugin/validate/EDINET/FormType.py +105 -0
  8. arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +111 -0
  9. arelle/plugin/validate/EDINET/ValidationPluginExtension.py +29 -0
  10. arelle/plugin/validate/EDINET/__init__.py +119 -0
  11. arelle/plugin/validate/EDINET/resources/config.xml +12 -0
  12. arelle/plugin/validate/EDINET/rules/__init__.py +0 -0
  13. arelle/plugin/validate/EDINET/rules/upload.py +321 -0
  14. arelle/plugin/validate/NL/rules/nl_kvk.py +52 -0
  15. arelle/utils/EntryPointDetection.py +6 -0
  16. {arelle_release-2.37.28.dist-info → arelle_release-2.37.30.dist-info}/METADATA +2 -1
  17. {arelle_release-2.37.28.dist-info → arelle_release-2.37.30.dist-info}/RECORD +62 -18
  18. tests/integration_tests/validation/conformance_suite_configs.py +2 -0
  19. tests/integration_tests/validation/conformance_suite_configurations/edinet.py +35 -0
  20. tests/integration_tests/validation/conformance_suite_configurations/nl_inline_2024.py +13 -3
  21. tests/integration_tests/validation/conformance_suite_configurations/xbrl_formula_1_0_function_registry.py +4 -0
  22. tests/integration_tests/validation/discover_tests.py +3 -3
  23. tests/integration_tests/validation/validation_util.py +9 -1
  24. tests/resources/conformance_suites/edinet/EC0121E/index.xml +23 -0
  25. tests/resources/conformance_suites/edinet/EC0121E/invalid01.zip +0 -0
  26. tests/resources/conformance_suites/edinet/EC0124E/index.xml +23 -0
  27. tests/resources/conformance_suites/edinet/EC0124E/invalid01.zip +0 -0
  28. tests/resources/conformance_suites/edinet/EC0129E/index.xml +23 -0
  29. tests/resources/conformance_suites/edinet/EC0129E/invalid01.zip +0 -0
  30. tests/resources/conformance_suites/edinet/EC0130E/index.xml +23 -0
  31. tests/resources/conformance_suites/edinet/EC0130E/invalid01.zip +0 -0
  32. tests/resources/conformance_suites/edinet/EC0132E/index.xml +23 -0
  33. tests/resources/conformance_suites/edinet/EC0132E/invalid01.zip +0 -0
  34. tests/resources/conformance_suites/edinet/EC0188E/index.xml +23 -0
  35. tests/resources/conformance_suites/edinet/EC0188E/invalid01.zip +0 -0
  36. tests/resources/conformance_suites/edinet/EC0198E/index.xml +23 -0
  37. tests/resources/conformance_suites/edinet/EC0198E/invalid01.zip +0 -0
  38. tests/resources/conformance_suites/edinet/README.md +4 -0
  39. tests/resources/conformance_suites/edinet/index.xml +11 -0
  40. tests/resources/conformance_suites/edinet/valid/index.xml +199 -0
  41. tests/resources/conformance_suites/edinet/valid/valid01.zip +0 -0
  42. tests/resources/conformance_suites/edinet/valid/valid02.zip +0 -0
  43. tests/resources/conformance_suites/edinet/valid/valid03.zip +0 -0
  44. tests/resources/conformance_suites/edinet/valid/valid04.zip +0 -0
  45. tests/resources/conformance_suites/edinet/valid/valid05.zip +0 -0
  46. tests/resources/conformance_suites/edinet/valid/valid06.zip +0 -0
  47. tests/resources/conformance_suites/edinet/valid/valid07.zip +0 -0
  48. tests/resources/conformance_suites/edinet/valid/valid08.zip +0 -0
  49. tests/resources/conformance_suites/edinet/valid/valid09.zip +0 -0
  50. tests/resources/conformance_suites/edinet/valid/valid10.zip +0 -0
  51. tests/resources/conformance_suites/edinet/valid/valid11.zip +0 -0
  52. tests/resources/conformance_suites/edinet/valid/valid12.zip +0 -0
  53. tests/resources/conformance_suites/edinet/valid/valid13.zip +0 -0
  54. tests/resources/conformance_suites/edinet/valid/valid14.zip +0 -0
  55. tests/resources/conformance_suites/edinet/valid/valid20.zip +0 -0
  56. tests/resources/conformance_suites/edinet/valid/valid21.zip +0 -0
  57. tests/resources/conformance_suites/edinet/valid/valid22.zip +0 -0
  58. tests/resources/conformance_suites_timing/edinet.json +27 -0
  59. {arelle_release-2.37.28.dist-info → arelle_release-2.37.30.dist-info}/WHEEL +0 -0
  60. {arelle_release-2.37.28.dist-info → arelle_release-2.37.30.dist-info}/entry_points.txt +0 -0
  61. {arelle_release-2.37.28.dist-info → arelle_release-2.37.30.dist-info}/licenses/LICENSE.md +0 -0
  62. {arelle_release-2.37.28.dist-info → arelle_release-2.37.30.dist-info}/top_level.txt +0 -0
arelle/Locale.py CHANGED
@@ -238,9 +238,11 @@ def getLocale() -> str | None:
238
238
  systemLocale = tryRunCommand("pwsh", "-Command", "Get-Culture | Select -ExpandProperty IetfLanguageTag")
239
239
  if pythonCompatibleLocale := findCompatibleLocale(systemLocale):
240
240
  _locale = pythonCompatibleLocale
241
- elif sys.version_info < (3, 12):
241
+ elif sys.version_info < (3, 12) or (3, 13, 3) <= sys.version_info[:3] <= (3, 13, 4):
242
242
  # Using locale.setlocale(...) because getlocale() in Python versions prior to 3.12 incorrectly aliased C.UTF-8 to en_US.UTF-8.
243
243
  # https://github.com/python/cpython/issues/74940
244
+ # Similar bug was reintroduced in Python 3.13.3 and fixed in 3.13.5.
245
+ # https://github.com/python/cpython/pull/135347
244
246
  _locale = locale.setlocale(locale.LC_CTYPE).partition(POSIX_LOCALE_ENCODING_SEPARATOR)[0]
245
247
  else:
246
248
  _locale = locale.getlocale()[0]
@@ -319,9 +319,15 @@ class ModelTestcaseVariation(ModelObject):
319
319
  errorElements = XmlUtil.descendants(self, None, "error")
320
320
  resultElement = XmlUtil.descendant(self, None, "result")
321
321
  if isinstance(errorElements,list) and len(errorElements) > 0:
322
- return [ModelValue.qname(e, e.stringValue) if not e.get("nonStandardErrorCodes")
323
- else e.stringValue
324
- for e in errorElements]
322
+ errorCodes = []
323
+ for errorElement in errorElements:
324
+ if errorElement.get("nonStandardErrorCodes"):
325
+ errorCode = errorElement.stringValue
326
+ else:
327
+ errorCode = ModelValue.qname(errorElement, errorElement.stringValue)
328
+ num = int(errorElement.attr("num") or 1)
329
+ errorCodes.extend([errorCode] * num)
330
+ return errorCodes
325
331
  #else:
326
332
  # errorElement = errorElements[1]
327
333
  # if errorElement is not None and not errorElement.get("nonStandardErrorCodes"):
arelle/Validate.py CHANGED
@@ -338,17 +338,18 @@ class Validate:
338
338
  loadedModels.append(modelXbrl)
339
339
  PackageManager.packageInfo(self.modelXbrl.modelManager.cntlr, readMeFirstUri, reload=True, errors=modelXbrl.errors)
340
340
  else: # not a multi-schemaRef versioning report
341
+ readMeFirstUriIsArchive = isReportPackageExtension(readMeFirstUri)
341
342
  readMeFirstUriIsEmbeddedZipFile = False
342
343
  if self.useFileSource.isArchive and not isLegacyAbs(readMeFirstUri):
343
- if isReportPackageExtension(readMeFirstUri):
344
+ if readMeFirstUriIsArchive:
344
345
  readMeFirstUriIsEmbeddedZipFile = True
345
346
  else:
346
347
  normalizedReadMeFirstUri = self.modelXbrl.modelManager.cntlr.webCache.normalizeUrl(readMeFirstUri, baseForElement)
347
348
  archivePath = FileSource.archiveFilenameParts(normalizedReadMeFirstUri)
348
349
  if archivePath:
349
350
  with self.useFileSource.fs.open(archivePath[1]) as embeddedFile:
350
- readMeFirstUriIsEmbeddedZipFile = zipfile.is_zipfile(embeddedFile)
351
- if not readMeFirstUriIsEmbeddedZipFile:
351
+ readMeFirstUriIsArchive = readMeFirstUriIsEmbeddedZipFile = zipfile.is_zipfile(embeddedFile)
352
+ if not readMeFirstUriIsArchive:
352
353
  modelXbrl = ModelXbrl.load(self.modelXbrl.modelManager,
353
354
  readMeFirstUri,
354
355
  _("validating"),
@@ -400,7 +401,16 @@ class Validate:
400
401
  # resolve an IXDS in entrypoints
401
402
  for pluginXbrlMethod in pluginClassMethods("ModelTestcaseVariation.ArchiveIxds"):
402
403
  pluginXbrlMethod(self, filesource,entrypoints)
403
- filesource.select(entrypoints[0].get("file", None))
404
+ for entrypoint in entrypoints:
405
+ filesource.select(entrypoint.get("file", None))
406
+ modelXbrl = ModelXbrl.load(self.modelXbrl.modelManager,
407
+ filesource,
408
+ _("validating"),
409
+ base=filesource.basefile + "/",
410
+ errorCaptureLevel=errorCaptureLevel,
411
+ ixdsTarget=modelTestcaseVariation.ixdsTarget,
412
+ errors=preLoadingErrors)
413
+ loadedModels.append(modelXbrl)
404
414
  except Exception as err:
405
415
  self.modelXbrl.error("exception:" + type(err).__name__,
406
416
  _("Testcase variation validation exception: %(error)s, entry URL: %(instance)s"),
@@ -427,15 +437,16 @@ class Validate:
427
437
  # Legacy ESEF conformance suite logic.
428
438
  for pluginXbrlMethod in pluginClassMethods("ModelTestcaseVariation.ReportPackageIxds"):
429
439
  filesource.select(pluginXbrlMethod(filesource, **_rptPkgIxdsOptions))
430
- modelXbrl = ModelXbrl.load(self.modelXbrl.modelManager,
431
- filesource,
432
- _("validating"),
433
- base=baseForElement,
434
- errorCaptureLevel=errorCaptureLevel,
435
- ixdsTarget=modelTestcaseVariation.ixdsTarget,
436
- isLoadable=modelTestcaseVariation.variationDiscoversDTS or filesource.url,
437
- errors=preLoadingErrors)
438
- loadedModels.append(modelXbrl)
440
+ if len(loadedModels) == 0:
441
+ modelXbrl = ModelXbrl.load(self.modelXbrl.modelManager,
442
+ filesource,
443
+ _("validating"),
444
+ base=baseForElement,
445
+ errorCaptureLevel=errorCaptureLevel,
446
+ ixdsTarget=modelTestcaseVariation.ixdsTarget,
447
+ isLoadable=modelTestcaseVariation.variationDiscoversDTS or filesource.url,
448
+ errors=preLoadingErrors)
449
+ loadedModels.append(modelXbrl)
439
450
 
440
451
  for model in loadedModels:
441
452
  modelXbrl.isTestcaseVariation = True
@@ -730,7 +741,11 @@ class Validate:
730
741
  indexPath = indexPath[len(baseZipFile) + 1:]
731
742
  indexPath = indexPath.replace("\\", "/")
732
743
  variationIdPath = f'{indexPath}:{modelTestcaseVariation.id}'
733
- if userExpectedErrors := testcaseExpectedErrors.get(variationIdPath):
744
+ userExpectedErrors = []
745
+ for userPattern, userErrors in testcaseExpectedErrors.items():
746
+ if fnmatch.fnmatch(variationIdPath, userPattern):
747
+ userExpectedErrors.extend(userErrors)
748
+ if userExpectedErrors:
734
749
  if expected is None:
735
750
  expected = []
736
751
  if isinstance(expected, str):
arelle/WebCache.py CHANGED
@@ -27,6 +27,13 @@ try:
27
27
  import ssl
28
28
  except ImportError:
29
29
  ssl = None
30
+
31
+ try:
32
+ import truststore
33
+ except ImportError:
34
+ # truststore requires Python > 3.9
35
+ truststore = None
36
+
30
37
  from arelle.FileSource import SERVER_WEB_CACHE, archiveFilenameParts
31
38
  from arelle.PluginManager import pluginClassMethods
32
39
  from arelle.UrlUtil import isHttpUrl
@@ -267,12 +274,10 @@ class WebCache:
267
274
  self.http_auth_handler = proxyhandlers.HTTPBasicAuthHandler()
268
275
  proxyHandlers = [self.proxy_handler, self.proxy_auth_handler, self.http_auth_handler]
269
276
  if ssl:
270
- # Attempts to load the default CA certificates from the OS.
271
- context = ssl.create_default_context()
277
+ # Attempt to load the default CA certificates from the OS using truststore if available, else fallback to OpenSSL's default context.
278
+ context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT) if truststore else ssl.create_default_context()
272
279
  # Include certifi certificates (Mozilla’s carefully curated
273
- # collection) for systems with outdated certs and for platforms
274
- # that we're unable to load certs from (macOS and some Linux
275
- # distros.)
280
+ # collection) for systems with outdated certs.
276
281
  context.load_verify_locations(cafile=certifi.where())
277
282
  if self.noCertificateCheck: # this is required in some Akamai environments, such as sec.gov
278
283
  context.check_hostname = False
arelle/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.37.28'
21
- __version_tuple__ = version_tuple = (2, 37, 28)
20
+ __version__ = version = '2.37.30'
21
+ __version_tuple__ = version_tuple = (2, 37, 30)
@@ -0,0 +1 @@
1
+ DISCLOSURE_SYSTEM_EDINET = 'EDINET'
@@ -0,0 +1,105 @@
1
+ """
2
+ See COPYRIGHT.md for copyright information.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from enum import Enum
7
+ from functools import cached_property, lru_cache
8
+ from pathlib import Path
9
+
10
+
11
+ class FormType(Enum):
12
+ ATTACH_DOC = "AttachDoc"
13
+ AUDIT_DOC = "AuditDoc"
14
+ ENGLISH_DOC = "EnglishDoc"
15
+ PRIVATE_ATTACH = "PrivateAttach"
16
+ PRIVATE_DOC = "PrivateDoc"
17
+ PUBLIC_DOC = "PublicDoc"
18
+
19
+ @classmethod
20
+ def parse(cls, value: str) -> FormType | None:
21
+ try:
22
+ return cls(value)
23
+ except ValueError:
24
+ return None
25
+
26
+ @cached_property
27
+ def extensionCategory(self) -> ExtensionCategory | None:
28
+ return FORM_TYPE_EXTENSION_CATEGORIES.get(self, None)
29
+
30
+ @cached_property
31
+ def manifestName(self) -> str:
32
+ return f'manifest_{self.value}.xml'
33
+
34
+ @cached_property
35
+ def manifestPath(self) -> Path:
36
+ return self.xbrlDirectory / self.manifestName
37
+
38
+ @cached_property
39
+ def xbrlDirectory(self) -> Path:
40
+ return Path('XBRL') / str(self.value)
41
+
42
+ @lru_cache(1)
43
+ def getValidExtensions(self, isAmendment: bool, isSubdirectory: bool) -> frozenset[str] | None:
44
+ if self.extensionCategory is None:
45
+ return None
46
+ return self.extensionCategory.getValidExtensions(isAmendment, isSubdirectory)
47
+
48
+
49
+ class ExtensionCategory(Enum):
50
+ ATTACH = 'ATTACH'
51
+ DOC = 'DOC'
52
+ ENGLISH_DOC = 'ENGLISH_DOC'
53
+
54
+ def getValidExtensions(self, isAmendment: bool, isSubdirectory: bool) -> frozenset[str] | None:
55
+ amendmentMap = VALID_EXTENSIONS[isAmendment]
56
+ categoryMap = amendmentMap.get(self, None)
57
+ if categoryMap is None:
58
+ return None
59
+ return categoryMap.get(isSubdirectory, None)
60
+
61
+
62
+ FORM_TYPE_EXTENSION_CATEGORIES = {
63
+ FormType.ATTACH_DOC: ExtensionCategory.ATTACH,
64
+ FormType.AUDIT_DOC: ExtensionCategory.DOC,
65
+ FormType.ENGLISH_DOC: ExtensionCategory.ENGLISH_DOC,
66
+ FormType.PRIVATE_ATTACH: ExtensionCategory.ATTACH,
67
+ FormType.PRIVATE_DOC: ExtensionCategory.DOC,
68
+ FormType.PUBLIC_DOC: ExtensionCategory.DOC,
69
+ }
70
+
71
+
72
+ HTML_EXTENSIONS = frozenset({'.htm', '.html', '.xhtml'})
73
+ IMAGE_EXTENSIONS = frozenset({'.jpeg', '.jpg', '.gif', '.png'})
74
+ ASSET_EXTENSIONS = frozenset(HTML_EXTENSIONS | IMAGE_EXTENSIONS)
75
+ XBRL_EXTENSIONS = frozenset(HTML_EXTENSIONS | {'.xml', '.xsd'})
76
+ ATTACH_EXTENSIONS = frozenset(HTML_EXTENSIONS | {'.pdf', })
77
+ ENGLISH_DOC_EXTENSIONS = frozenset(ASSET_EXTENSIONS | frozenset({'.pdf', '.xml', '.txt'}))
78
+
79
+ # Is Amendment -> Category -> Is Subdirectory
80
+ VALID_EXTENSIONS = {
81
+ False: {
82
+ ExtensionCategory.ATTACH: {
83
+ False: ATTACH_EXTENSIONS,
84
+ True: ASSET_EXTENSIONS,
85
+ },
86
+ ExtensionCategory.DOC: {
87
+ False: XBRL_EXTENSIONS,
88
+ True: ASSET_EXTENSIONS
89
+ },
90
+ },
91
+ True: {
92
+ ExtensionCategory.ATTACH: {
93
+ False: ATTACH_EXTENSIONS,
94
+ True: ASSET_EXTENSIONS,
95
+ },
96
+ ExtensionCategory.DOC: {
97
+ False: HTML_EXTENSIONS,
98
+ True: ASSET_EXTENSIONS,
99
+ },
100
+ ExtensionCategory.ENGLISH_DOC: {
101
+ False: ENGLISH_DOC_EXTENSIONS,
102
+ True: ENGLISH_DOC_EXTENSIONS,
103
+ },
104
+ },
105
+ }
@@ -0,0 +1,111 @@
1
+ """
2
+ See COPYRIGHT.md for copyright information.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import zipfile
7
+ from collections import defaultdict
8
+ from dataclasses import dataclass
9
+ from functools import lru_cache
10
+ from pathlib import Path
11
+
12
+ from arelle.ModelXbrl import ModelXbrl
13
+ from arelle.ValidateXbrl import ValidateXbrl
14
+ from arelle.typing import TypeGetText
15
+ from arelle.utils.PluginData import PluginData
16
+ from .FormType import FormType
17
+
18
+ _: TypeGetText
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class UploadContents:
23
+ amendmentPaths: dict[FormType, frozenset[Path]]
24
+ directories: frozenset[Path]
25
+ forms: dict[FormType, frozenset[Path]]
26
+ unknownPaths: frozenset[Path]
27
+
28
+
29
+ @dataclass
30
+ class PluginValidationDataExtension(PluginData):
31
+ _primaryModelXbrl: ModelXbrl | None = None
32
+
33
+ # Identity hash for caching.
34
+ def __hash__(self) -> int:
35
+ return id(self)
36
+
37
+ @lru_cache(1)
38
+ def shouldValidateUpload(self, val: ValidateXbrl) -> bool:
39
+ """
40
+ Determine if the upload validation should be performed on this model.
41
+
42
+ Upload validation should not be performed if the target document is
43
+ not a zipfile.
44
+
45
+ Upload validation should only be performed once for the entire package,
46
+ not duplicated for each model. To facilitate this with Arelle's validation
47
+ system which largely prevents referencing other models, we can use `--keepOpen`
48
+ and check if the given model is the first to be loaded.
49
+ :param val: The ValidateXbrl instance with a model to check.
50
+ :return: True if upload validation should be performed, False otherwise.
51
+ """
52
+ modelXbrl = val.modelXbrl
53
+ if modelXbrl == val.testModelXbrl:
54
+ # Not running within a testcase
55
+ if modelXbrl != modelXbrl.modelManager.loadedModelXbrls[0]:
56
+ return False
57
+ if not modelXbrl.fileSource.fs:
58
+ return False # No stream
59
+ if not isinstance(modelXbrl.fileSource.fs, zipfile.ZipFile):
60
+ return False # Not a zipfile
61
+ return True
62
+
63
+ @lru_cache(1)
64
+ def getUploadContents(self, modelXbrl: ModelXbrl) -> UploadContents:
65
+ uploadFilepaths = self.getUploadFilepaths(modelXbrl)
66
+ amendmentPaths = defaultdict(list)
67
+ unknownPaths = []
68
+ directories = []
69
+ forms = defaultdict(list)
70
+ for path in uploadFilepaths:
71
+ parents = list(reversed([p.name for p in path.parents if len(p.name) > 0]))
72
+ if len(parents) == 0:
73
+ continue
74
+ if parents[0] == 'XBRL':
75
+ if len(parents) > 1:
76
+ formName = parents[1]
77
+ formType = FormType.parse(formName)
78
+ if formType is not None:
79
+ forms[formType].append(path)
80
+ continue
81
+ formName = parents[0]
82
+ formType = FormType.parse(formName)
83
+ if formType is not None:
84
+ amendmentPaths[formType].append(path)
85
+ continue
86
+ if len(path.suffix) == 0:
87
+ directories.append(path)
88
+ continue
89
+ unknownPaths.append(path)
90
+ return UploadContents(
91
+ amendmentPaths={k: frozenset(v) for k, v in amendmentPaths.items() if len(v) > 0},
92
+ directories=frozenset(directories),
93
+ forms={k: frozenset(v) for k, v in forms.items() if len(v) > 0},
94
+ unknownPaths=frozenset(unknownPaths)
95
+ )
96
+
97
+ @lru_cache(1)
98
+ def getUploadFilepaths(self, modelXbrl: ModelXbrl) -> list[Path]:
99
+ if not modelXbrl.fileSource.fs or \
100
+ not isinstance(modelXbrl.fileSource.fs, zipfile.ZipFile):
101
+ modelXbrl.warning(
102
+ codes="EDINET.uploadNotValidated",
103
+ msg=_("The target file is not a zip file, so upload validation could not be performed.")
104
+ )
105
+ return []
106
+ paths = set()
107
+ for name in modelXbrl.fileSource.fs.namelist():
108
+ path = Path(name)
109
+ paths.add(path)
110
+ paths.update(path.parents)
111
+ return sorted(paths)
@@ -0,0 +1,29 @@
1
+ """
2
+ See COPYRIGHT.md for copyright information.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+ from arelle.ValidateXbrl import ValidateXbrl
9
+ from arelle.typing import TypeGetText
10
+ from arelle.utils.validate.ValidationPlugin import ValidationPlugin
11
+ from .DisclosureSystems import DISCLOSURE_SYSTEM_EDINET
12
+ from .PluginValidationDataExtension import PluginValidationDataExtension
13
+
14
+ _: TypeGetText
15
+
16
+
17
+ class ValidationPluginExtension(ValidationPlugin):
18
+ def newPluginData(self, validateXbrl: ValidateXbrl) -> PluginValidationDataExtension:
19
+ disclosureSystem = validateXbrl.disclosureSystem.name
20
+ if disclosureSystem == DISCLOSURE_SYSTEM_EDINET:
21
+ pass
22
+ else:
23
+ raise ValueError(f'Invalid EDINET disclosure system: {disclosureSystem}')
24
+ return PluginValidationDataExtension(
25
+ self.name,
26
+ )
27
+
28
+ def modelXbrlLoadComplete(self, *args: Any, **kwargs: Any) -> None:
29
+ return None
@@ -0,0 +1,119 @@
1
+ """
2
+ See COPYRIGHT.md for copyright information.
3
+ - [Operation Guides](https://disclosure2dl.edinet-fsa.go.jp/guide/static/disclosure/WEEK0060.html)
4
+ - [Document Search](https://disclosure2.edinet-fsa.go.jp/week0020.aspx)
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import zipfile
9
+ from collections import defaultdict
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from lxml import etree
14
+ from lxml.etree import _Element
15
+
16
+ from arelle.FileSource import FileSource
17
+ from arelle.Version import authorLabel, copyrightLabel
18
+ from .ValidationPluginExtension import ValidationPluginExtension
19
+ from .rules import upload
20
+
21
+ PLUGIN_NAME = "Validate EDINET"
22
+ DISCLOSURE_SYSTEM_VALIDATION_TYPE = "EDINET"
23
+
24
+
25
+ validationPlugin = ValidationPluginExtension(
26
+ name=PLUGIN_NAME,
27
+ disclosureSystemConfigUrl=Path(__file__).parent / "resources" / "config.xml",
28
+ validationTypes=[DISCLOSURE_SYSTEM_VALIDATION_TYPE],
29
+ validationRuleModules=[
30
+ upload,
31
+ ],
32
+ )
33
+
34
+
35
+ def disclosureSystemTypes(*args: Any, **kwargs: Any) -> tuple[tuple[str, str], ...]:
36
+ return validationPlugin.disclosureSystemTypes
37
+
38
+
39
+ def disclosureSystemConfigURL(*args: Any, **kwargs: Any) -> str:
40
+ return validationPlugin.disclosureSystemConfigURL
41
+
42
+
43
+ def _parseManifestDoc(xmlRootElement: _Element, base: Path) -> dict[str, list[Path]]:
44
+ sets = defaultdict(list)
45
+ for instanceElt in xmlRootElement.iter(tag="{http://disclosure.edinet-fsa.go.jp/2013/manifest}instance"):
46
+ instanceId = str(instanceElt.attrib["id"])
47
+ for ixbrlElt in instanceElt.iter(tag="{http://disclosure.edinet-fsa.go.jp/2013/manifest}ixbrl"):
48
+ uri = ixbrlElt.text.strip() if ixbrlElt.text is not None else None
49
+ if uri:
50
+ sets[instanceId].append(base / uri)
51
+ return sets
52
+
53
+
54
+ def fileSourceEntrypointFiles(filesource: FileSource, inlineOnly: bool, *args: Any, **kwargs: Any) -> list[dict[str, Any]] | None:
55
+ manifests = {}
56
+ if filesource.isArchive:
57
+ if filesource.isTaxonomyPackage:
58
+ return None
59
+ if filesource.reportPackage is not None:
60
+ return None
61
+ for _archiveFile in (filesource.dir or ()):
62
+ if not Path(_archiveFile).stem.startswith('manifest'):
63
+ continue
64
+ assert isinstance(filesource.fs, zipfile.ZipFile), \
65
+ "The EDINET plugin only supports archives in .zip format."
66
+ with filesource.fs.open(_archiveFile) as manifestDoc:
67
+ base = Path(_archiveFile).parent
68
+ xmlRootElement = etree.fromstring(manifestDoc.read())
69
+ manifests.update(_parseManifestDoc(xmlRootElement, base))
70
+ elif (dirpath := Path(str(filesource.url))).is_dir():
71
+ for file in dirpath.rglob("*"):
72
+ if not file.is_file():
73
+ continue
74
+ if not file.stem.startswith('manifest'):
75
+ continue
76
+ with open(file, 'rb') as manifestDoc:
77
+ base = file.parent
78
+ xmlRootElement = etree.fromstring(manifestDoc.read())
79
+ manifests.update(_parseManifestDoc(xmlRootElement, base))
80
+ if len(manifests) == 0:
81
+ return None
82
+
83
+ entrypointFiles = []
84
+ for instanceId, uris in manifests.items():
85
+ entrypoints = []
86
+ for uri in uris:
87
+ filesource.select(str(uri))
88
+ entrypoints.append({"file": filesource.url})
89
+ entrypointFiles.append({'ixds': entrypoints})
90
+ return entrypointFiles
91
+
92
+
93
+ def modelXbrlLoadComplete(*args: Any, **kwargs: Any) -> None:
94
+ return validationPlugin.modelXbrlLoadComplete(*args, **kwargs)
95
+
96
+
97
+ def validateFinally(*args: Any, **kwargs: Any) -> None:
98
+ return validationPlugin.validateFinally(*args, **kwargs)
99
+
100
+
101
+ def validateXbrlFinally(*args: Any, **kwargs: Any) -> None:
102
+ return validationPlugin.validateXbrlFinally(*args, **kwargs)
103
+
104
+
105
+ __pluginInfo__ = {
106
+ "name": PLUGIN_NAME,
107
+ "version": "0.0.1",
108
+ "description": "Validation plugin for the EDINET taxonomies.",
109
+ "license": "Apache-2",
110
+ "author": authorLabel,
111
+ "copyright": copyrightLabel,
112
+ "import": ("inlineXbrlDocumentSet",),
113
+ "DisclosureSystem.Types": disclosureSystemTypes,
114
+ "DisclosureSystem.ConfigURL": disclosureSystemConfigURL,
115
+ "FileSource.EntrypointFiles": fileSourceEntrypointFiles,
116
+ "ModelXbrl.LoadComplete": modelXbrlLoadComplete,
117
+ "Validate.XBRL.Finally": validateXbrlFinally,
118
+ "ValidateFormula.Finished": validateFinally,
119
+ }
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <DisclosureSystems
3
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+ xsi:noNamespaceSchemaLocation="../../../../config/disclosuresystems.xsd">
5
+ <!-- see arelle/config/disclosuresystems.xml for full comments -->
6
+ <DisclosureSystem
7
+ names="EDINET|edinet"
8
+ description="Checks for EDINET."
9
+ validationType="EDINET"
10
+ exclusiveTypesPattern="EFM|GFM|FERC|HMRC|SBR.NL|EBA|EIOPA|ESEF"
11
+ />
12
+ </DisclosureSystems>
File without changes