arelle-release 2.37.50__py3-none-any.whl → 2.37.51__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.

arelle/_version.py CHANGED
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '2.37.50'
21
- __version_tuple__ = version_tuple = (2, 37, 50)
31
+ __version__ = version = '2.37.51'
32
+ __version_tuple__ = version_tuple = (2, 37, 51)
33
+
34
+ __commit_id__ = commit_id = None
@@ -37,3 +37,5 @@ qnEdinetManifestList = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}
37
37
  qnEdinetManifestTitle = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}title")
38
38
  qnEdinetManifestTocComposition = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}tocComposition")
39
39
  xhtmlDtdExtension = "xhtml1-strict-ix.dtd"
40
+
41
+ COVER_PAGE_FILENAME_PREFIX = "0000000_header_"
@@ -12,11 +12,11 @@ from typing import TYPE_CHECKING
12
12
 
13
13
  from arelle.Cntlr import Cntlr
14
14
  from arelle.FileSource import FileSource
15
- from arelle.ModelXbrl import ModelXbrl
16
15
  from arelle.typing import TypeGetText
17
16
  from arelle.utils.PluginData import PluginData
18
- from .InstanceType import InstanceType
19
- from .UploadContents import UploadContents
17
+ from . import Constants
18
+ from .ReportFolderType import ReportFolderType
19
+ from .UploadContents import UploadContents, UploadPathInfo
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  from .ManifestInstance import ManifestInstance
@@ -50,35 +50,46 @@ class ControllerPluginData(PluginData):
50
50
  @lru_cache(1)
51
51
  def getUploadContents(self, fileSource: FileSource) -> UploadContents:
52
52
  uploadFilepaths = self.getUploadFilepaths(fileSource)
53
- amendmentPaths = defaultdict(list)
54
- unknownPaths = []
55
- directories = []
56
- forms = defaultdict(list)
53
+ reports = defaultdict(list)
54
+ uploadPaths = {}
57
55
  for path in uploadFilepaths:
58
- parents = list(reversed([p.name for p in path.parents if len(p.name) > 0]))
59
- if len(parents) == 0:
60
- continue
61
- if parents[0] == 'XBRL':
62
- if len(parents) > 1:
63
- formName = parents[1]
64
- instanceType = InstanceType.parse(formName)
65
- if instanceType is not None:
66
- forms[instanceType].append(path)
67
- continue
68
- formName = parents[0]
69
- instanceType = InstanceType.parse(formName)
70
- if instanceType is not None:
71
- amendmentPaths[instanceType].append(path)
56
+ if len(path.parts) == 0:
72
57
  continue
73
- if len(path.suffix) == 0:
74
- directories.append(path)
75
- continue
76
- unknownPaths.append(path)
58
+ parents = list(reversed([p.name for p in path.parents if len(p.name) > 0]))
59
+ reportFolderType = None
60
+ isCorrection = True
61
+ isDirectory = len(path.suffix) == 0
62
+ isInSubdirectory = False
63
+ reportPath = None
64
+ if len(parents) > 0:
65
+ isCorrection = parents[0] != 'XBRL'
66
+ if not isCorrection:
67
+ if len(parents) > 1:
68
+ formName = parents[1]
69
+ isInSubdirectory = len(parents) > 2
70
+ reportFolderType = ReportFolderType.parse(formName)
71
+ if reportFolderType is None:
72
+ formName = parents[0]
73
+ isInSubdirectory = len(parents) > 1
74
+ reportFolderType = ReportFolderType.parse(formName)
75
+ if reportFolderType is not None:
76
+ reportPath = Path(reportFolderType.value) if isCorrection else Path("XBRL") / reportFolderType.value
77
+ if not isCorrection:
78
+ reports[reportFolderType].append(path)
79
+ uploadPaths[path] = UploadPathInfo(
80
+ isAttachment=reportFolderType is not None and reportFolderType.isAttachment,
81
+ isCorrection=isCorrection,
82
+ isCoverPage=not isDirectory and path.stem.startswith(Constants.COVER_PAGE_FILENAME_PREFIX),
83
+ isDirectory=len(path.suffix) == 0,
84
+ isRoot=len(path.parts) == 1,
85
+ isSubdirectory=isInSubdirectory or (isDirectory and reportFolderType is not None),
86
+ path=path,
87
+ reportFolderType=reportFolderType,
88
+ reportPath=reportPath,
89
+ )
77
90
  return UploadContents(
78
- amendmentPaths={k: frozenset(v) for k, v in amendmentPaths.items() if len(v) > 0},
79
- directories=frozenset(directories),
80
- instances={k: frozenset(v) for k, v in forms.items() if len(v) > 0},
81
- unknownPaths=frozenset(unknownPaths)
91
+ reports={k: frozenset(v) for k, v in reports.items() if len(v) > 0},
92
+ uploadPaths=uploadPaths
82
93
  )
83
94
 
84
95
  @lru_cache(1)
@@ -7,15 +7,17 @@ from collections import defaultdict
7
7
  from dataclasses import dataclass
8
8
  from decimal import Decimal
9
9
  from functools import lru_cache
10
+ from lxml.etree import DTD, XML
10
11
  from operator import attrgetter
11
12
  from typing import Callable, Hashable, Iterable, cast
12
13
 
14
+ import os
13
15
  import regex
14
16
 
15
17
  from arelle.LinkbaseType import LinkbaseType
16
18
  from arelle.ModelDocument import Type as ModelDocumentType
17
19
  from arelle.ModelDtsObject import ModelConcept
18
- from arelle.ModelInstanceObject import ModelFact, ModelUnit, ModelContext
20
+ from arelle.ModelInstanceObject import ModelFact, ModelUnit, ModelContext, ModelInlineFact
19
21
  from arelle.ModelObject import ModelObject
20
22
  from arelle.ModelValue import QName, qname
21
23
  from arelle.ModelXbrl import ModelXbrl
@@ -24,7 +26,7 @@ from arelle.ValidateDuplicateFacts import getDeduplicatedFacts, DeduplicationTyp
24
26
  from arelle.XmlValidate import VALID
25
27
  from arelle.typing import TypeGetText
26
28
  from arelle.utils.PluginData import PluginData
27
- from .Constants import CORPORATE_FORMS, FormType
29
+ from .Constants import CORPORATE_FORMS, FormType, xhtmlDtdExtension
28
30
  from .ControllerPluginData import ControllerPluginData
29
31
  from .ManifestInstance import ManifestInstance
30
32
  from .Statement import Statement, STATEMENTS, BalanceSheet, StatementInstance, StatementType
@@ -166,6 +168,22 @@ class PluginValidationDataExtension(PluginData):
166
168
  )
167
169
  return balanceSheets
168
170
 
171
+ def getProblematicTextBlocks(self, modelXbrl: ModelXbrl) -> list[ModelInlineFact]:
172
+ problematicTextBlocks: list[ModelInlineFact] = []
173
+ dtd = DTD(os.path.join(modelXbrl.modelManager.cntlr.configDir, xhtmlDtdExtension))
174
+ htmlBodyTemplate = "<body><div>\n{0}\n</div></body>\n"
175
+ for fact in modelXbrl.facts:
176
+ concept = fact.concept
177
+ if isinstance(fact, ModelInlineFact) and not fact.isNil and concept is not None and concept.isTextBlock and not fact.isEscaped:
178
+ xmlBody = htmlBodyTemplate.format(fact.value)
179
+ try:
180
+ textblockXml = XML(xmlBody)
181
+ if not dtd.validate(textblockXml):
182
+ problematicTextBlocks.append(fact)
183
+ except Exception:
184
+ problematicTextBlocks.append(fact)
185
+ return problematicTextBlocks
186
+
169
187
  @lru_cache(1)
170
188
  def getStatementInstance(self, modelXbrl: ModelXbrl, statement: Statement) -> StatementInstance | None:
171
189
  if statement.roleUri not in modelXbrl.roleTypes:
@@ -8,16 +8,15 @@ from functools import cached_property, lru_cache
8
8
  from pathlib import Path
9
9
 
10
10
 
11
- class InstanceType(Enum):
11
+ class ReportFolderType(Enum):
12
12
  ATTACH_DOC = "AttachDoc"
13
13
  AUDIT_DOC = "AuditDoc"
14
- ENGLISH_DOC = "EnglishDoc"
15
14
  PRIVATE_ATTACH = "PrivateAttach"
16
15
  PRIVATE_DOC = "PrivateDoc"
17
16
  PUBLIC_DOC = "PublicDoc"
18
17
 
19
18
  @classmethod
20
- def parse(cls, value: str) -> InstanceType | None:
19
+ def parse(cls, value: str) -> ReportFolderType | None:
21
20
  try:
22
21
  return cls(value)
23
22
  except ValueError:
@@ -27,6 +26,10 @@ class InstanceType(Enum):
27
26
  def extensionCategory(self) -> ExtensionCategory | None:
28
27
  return FORM_TYPE_EXTENSION_CATEGORIES.get(self, None)
29
28
 
29
+ @cached_property
30
+ def isAttachment(self) -> bool:
31
+ return "Attach" in self.value
32
+
30
33
  @cached_property
31
34
  def manifestName(self) -> str:
32
35
  return f'manifest_{self.value}.xml'
@@ -49,7 +52,6 @@ class InstanceType(Enum):
49
52
  class ExtensionCategory(Enum):
50
53
  ATTACH = 'ATTACH'
51
54
  DOC = 'DOC'
52
- ENGLISH_DOC = 'ENGLISH_DOC'
53
55
 
54
56
  def getValidExtensions(self, isAmendment: bool, isSubdirectory: bool) -> frozenset[str] | None:
55
57
  amendmentMap = VALID_EXTENSIONS[isAmendment]
@@ -60,12 +62,11 @@ class ExtensionCategory(Enum):
60
62
 
61
63
 
62
64
  FORM_TYPE_EXTENSION_CATEGORIES = {
63
- InstanceType.ATTACH_DOC: ExtensionCategory.ATTACH,
64
- InstanceType.AUDIT_DOC: ExtensionCategory.DOC,
65
- InstanceType.ENGLISH_DOC: ExtensionCategory.ENGLISH_DOC,
66
- InstanceType.PRIVATE_ATTACH: ExtensionCategory.ATTACH,
67
- InstanceType.PRIVATE_DOC: ExtensionCategory.DOC,
68
- InstanceType.PUBLIC_DOC: ExtensionCategory.DOC,
65
+ ReportFolderType.ATTACH_DOC: ExtensionCategory.ATTACH,
66
+ ReportFolderType.AUDIT_DOC: ExtensionCategory.DOC,
67
+ ReportFolderType.PRIVATE_ATTACH: ExtensionCategory.ATTACH,
68
+ ReportFolderType.PRIVATE_DOC: ExtensionCategory.DOC,
69
+ ReportFolderType.PUBLIC_DOC: ExtensionCategory.DOC,
69
70
  }
70
71
 
71
72
 
@@ -74,7 +75,6 @@ IMAGE_EXTENSIONS = frozenset({'.jpeg', '.jpg', '.gif', '.png'})
74
75
  ASSET_EXTENSIONS = frozenset(HTML_EXTENSIONS | IMAGE_EXTENSIONS)
75
76
  XBRL_EXTENSIONS = frozenset(HTML_EXTENSIONS | {'.xml', '.xsd'})
76
77
  ATTACH_EXTENSIONS = frozenset(HTML_EXTENSIONS | {'.pdf', })
77
- ENGLISH_DOC_EXTENSIONS = frozenset(ASSET_EXTENSIONS | frozenset({'.pdf', '.xml', '.txt'}))
78
78
 
79
79
  # Is Amendment -> Category -> Is Subdirectory
80
80
  VALID_EXTENSIONS = {
@@ -97,9 +97,5 @@ VALID_EXTENSIONS = {
97
97
  False: HTML_EXTENSIONS,
98
98
  True: ASSET_EXTENSIONS,
99
99
  },
100
- ExtensionCategory.ENGLISH_DOC: {
101
- False: ENGLISH_DOC_EXTENSIONS,
102
- True: ENGLISH_DOC_EXTENSIONS,
103
- },
104
100
  },
105
101
  }
@@ -6,12 +6,27 @@ from __future__ import annotations
6
6
  from dataclasses import dataclass
7
7
  from pathlib import Path
8
8
 
9
- from .InstanceType import InstanceType
9
+ from .ReportFolderType import ReportFolderType
10
10
 
11
11
 
12
12
  @dataclass(frozen=True)
13
13
  class UploadContents:
14
- amendmentPaths: dict[InstanceType, frozenset[Path]]
15
- directories: frozenset[Path]
16
- instances: dict[InstanceType, frozenset[Path]]
17
- unknownPaths: frozenset[Path]
14
+ reports: dict[ReportFolderType, frozenset[Path]]
15
+ uploadPaths: dict[Path, UploadPathInfo]
16
+
17
+ @property
18
+ def sortedPaths(self) -> list[Path]:
19
+ return sorted(self.uploadPaths.keys())
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class UploadPathInfo:
24
+ isAttachment: bool
25
+ isCorrection: bool
26
+ isCoverPage: bool
27
+ isDirectory: bool
28
+ isRoot: bool
29
+ isSubdirectory: bool
30
+ path: Path
31
+ reportFolderType: ReportFolderType | None
32
+ reportPath: Path | None
@@ -30,7 +30,7 @@ from arelle.utils.validate.Validation import Validation
30
30
  from arelle.utils.validate.ValidationUtil import etreeIterWithDepth
31
31
  from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
32
32
  from ..PluginValidationDataExtension import PluginValidationDataExtension
33
- from ..Constants import xhtmlDtdExtension
33
+
34
34
 
35
35
  _: TypeGetText
36
36
 
@@ -368,28 +368,15 @@ def rule_gfm_1_2_14(
368
368
  (a format that conforms to XML grammar, such as all start and end tags being paired, and the end tag of a nested tag not coming after the end tag of its parent tag, etc.).
369
369
  Please modify it so that it is well-formed.
370
370
  """
371
- CDATApattern = regex.compile(r"<!\[CDATA\[(.+)\]\]")
372
- dtd = DTD(os.path.join(val.modelXbrl.modelManager.cntlr.configDir, xhtmlDtdExtension))
373
- htmlBodyTemplate = "<body><div>\n{0}\n</div></body>\n"
374
- namedEntityPattern = regex.compile("&[_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\xFF\u0100-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]"
375
- r"[_\-\.:"
376
- "\xB7A-Za-z0-9\xC0-\xD6\xD8-\xF6\xF8-\xFF\u0100-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u0300-\u036F\u203F-\u2040]*;")
377
- XMLpattern = regex.compile(r".*(<|&lt;|&#x3C;|&#60;)[A-Za-z_]+[A-Za-z0-9_:]*[^>]*(/>|>|&gt;|/&gt;).*", regex.DOTALL)
378
-
379
- for fact in val.modelXbrl.facts:
380
- concept = fact.concept
381
- if not fact.isNil and concept is not None and concept.isTextBlock and XMLpattern.match(fact.value):
382
- for xmlText in [fact.value] + CDATApattern.findall(fact.value):
383
- xmlBodyWithoutEntities = htmlBodyTemplate.format(namedEntityPattern.sub("", xmlText).replace('&','&amp;'))
384
- textblockXml = XML(xmlBodyWithoutEntities)
385
- if not dtd.validate(textblockXml):
386
- yield Validation.warning(
387
- codes='EDINET.EC5700W.GFM.1.2.14',
388
- msg=_('The content of an element with a data type of nonnum:textBlockItemType is not well-formed XML (a format that conforms to XML grammar, '
389
- 'such as all start and end tags being in pairs, and the end tag of a nested tag not coming after the end tag of its parent tag). '
390
- 'Correct the content so that it is well-formed.'),
391
- modelObject = fact
392
- )
371
+ problematicFacts = pluginData.getProblematicTextBlocks(val.modelXbrl)
372
+ if len(problematicFacts) > 0:
373
+ yield Validation.warning(
374
+ codes='EDINET.EC5700W.GFM.1.2.14',
375
+ msg=_('The content of an element with a data type of nonnum:textBlockItemType is not well-formed XML (a format that conforms to XML grammar, '
376
+ 'such as all start and end tags being in pairs, and the end tag of a nested tag not coming after the end tag of its parent tag). '
377
+ 'Correct the content so that it is well-formed.'),
378
+ modelObject = problematicFacts
379
+ )
393
380
 
394
381
 
395
382
  @validation(
@@ -16,7 +16,7 @@ from arelle.utils.PluginHooks import ValidationHook
16
16
  from arelle.utils.validate.Decorator import validation
17
17
  from arelle.utils.validate.Validation import Validation
18
18
  from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
19
- from ..InstanceType import InstanceType, HTML_EXTENSIONS, IMAGE_EXTENSIONS
19
+ from ..ReportFolderType import ReportFolderType, HTML_EXTENSIONS, IMAGE_EXTENSIONS
20
20
  from ..PluginValidationDataExtension import PluginValidationDataExtension
21
21
 
22
22
  if TYPE_CHECKING:
@@ -24,6 +24,16 @@ if TYPE_CHECKING:
24
24
 
25
25
  _: TypeGetText
26
26
 
27
+ ALLOWED_ROOT_FOLDERS = {
28
+ "AttachDoc",
29
+ "AuditDoc",
30
+ "PrivateAttach",
31
+ "PrivateDoc",
32
+ "PublicAttach",
33
+ "PublicDoc",
34
+ "XBRL",
35
+ }
36
+
27
37
  FILE_COUNT_LIMITS = {
28
38
  Path("AttachDoc"): 990,
29
39
  Path("AuditDoc"): 990,
@@ -43,7 +53,7 @@ FILENAME_STEM_PATTERN = re.compile(r'[a-zA-Z0-9_-]*')
43
53
  hook=ValidationHook.FILESOURCE,
44
54
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
45
55
  )
46
- def rule_EC0121E(
56
+ def rule_EC0100E(
47
57
  pluginData: ControllerPluginData,
48
58
  cntlr: Cntlr,
49
59
  fileSource: FileSource,
@@ -51,30 +61,32 @@ def rule_EC0121E(
51
61
  **kwargs: Any,
52
62
  ) -> Iterable[Validation]:
53
63
  """
54
- EDINET.EC0121E: There is a directory or file that contains more than 31 characters
55
- or uses characters other than those allowed (alphanumeric characters, '-' and '_').
56
-
57
- Note: Sample instances from EDINET almost always violate this rule based on our
58
- current interpretation. The exception being files placed outside the XBRL directory,
59
- i.e. amendment documents. For now, we will only check amendment documents, directory
60
- names, or other files in unexpected locations.
64
+ EDINET.EC0100E: An illegal directory is found directly under the transferred directory.
65
+ Only the following root folders are allowed:
66
+ AttachDoc
67
+ AuditDoc*
68
+ PrivateAttach
69
+ PrivateDoc*
70
+ PublicAttach
71
+ PublicDoc*
72
+ XBRL
73
+ * Only when reporting corrections
74
+
75
+ NOTE: since we do not have access to the submission type, we can't determine if the submission is a correction or not.
76
+ For this implementation, we will allow all directories that may be valid for at least one submission type.
77
+ This allows for a false-negative outcome when a non-correction submission has a correction-only root directory.
61
78
  """
62
79
  uploadContents = pluginData.getUploadContents(fileSource)
63
- paths = set(uploadContents.directories | uploadContents.unknownPaths)
64
- for amendmentPaths in uploadContents.amendmentPaths.values():
65
- paths.update(amendmentPaths)
66
- for path in paths:
67
- if len(str(path.name)) > 31 or not FILENAME_STEM_PATTERN.match(path.stem):
80
+ for path, pathInfo in uploadContents.uploadPaths.items():
81
+ if pathInfo.isRoot and path.name not in ALLOWED_ROOT_FOLDERS:
68
82
  yield Validation.error(
69
- codes='EDINET.EC0121E',
70
- msg=_("There is a directory or file in '%(directory)s' that contains more than 31 characters "
71
- "or uses characters other than those allowed (alphanumeric characters, '-' and '_'). "
72
- "Directory or file name: '%(basename)s'. "
73
- "Please change the file name (or folder name) to within 31 characters and to usable "
74
- "characters, and upload again."),
75
- directory=str(path.parent),
76
- basename=path.name,
77
- file=str(path)
83
+ codes='EDINET.EC0100E',
84
+ msg=_("An illegal directory is found directly under the transferred directory. "
85
+ "Directory name or file name: '%(rootDirectory)s'. "
86
+ "Delete all folders except the following folders that exist directly "
87
+ "under the root folder, and then upload again: %(allowedDirectories)s."),
88
+ rootDirectory=path.name,
89
+ allowedDirectories=', '.join(f"'{d}'" for d in ALLOWED_ROOT_FOLDERS)
78
90
  )
79
91
 
80
92
 
@@ -82,7 +94,7 @@ def rule_EC0121E(
82
94
  hook=ValidationHook.FILESOURCE,
83
95
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
84
96
  )
85
- def rule_EC0124E(
97
+ def rule_EC0124E_EC0187E(
86
98
  pluginData: ControllerPluginData,
87
99
  cntlr: Cntlr,
88
100
  fileSource: FileSource,
@@ -90,7 +102,8 @@ def rule_EC0124E(
90
102
  **kwargs: Any,
91
103
  ) -> Iterable[Validation]:
92
104
  """
93
- EDINET.EC0124E: There are no empty directories.
105
+ EDINET.EC0124E: There are no empty root directories.
106
+ EDINET.EC0187E: There are no empty subdirectories.
94
107
  """
95
108
  uploadFilepaths = pluginData.getUploadFilepaths(fileSource)
96
109
  emptyDirectories = []
@@ -98,15 +111,24 @@ def rule_EC0124E(
98
111
  if path.suffix:
99
112
  continue
100
113
  if not any(path in p.parents for p in uploadFilepaths):
101
- emptyDirectories.append(str(path))
114
+ emptyDirectories.append(path)
102
115
  for emptyDirectory in emptyDirectories:
103
- yield Validation.error(
104
- codes='EDINET.EC0124E',
105
- msg=_("There is no file directly under '%(emptyDirectory)s'. "
106
- "No empty folders. "
107
- "Please store the file in the appropriate folder or delete the folder and upload again."),
108
- emptyDirectory=emptyDirectory,
109
- )
116
+ if len(emptyDirectory.parts) <= 1:
117
+ yield Validation.error(
118
+ codes='EDINET.EC0124E',
119
+ msg=_("There is no file directly under '%(emptyDirectory)s'. "
120
+ "No empty root folders. "
121
+ "Please store the file in the appropriate folder or delete the folder and upload again."),
122
+ emptyDirectory=str(emptyDirectory),
123
+ )
124
+ else:
125
+ yield Validation.error(
126
+ codes='EDINET.EC0187E',
127
+ msg=_("'%(parentDirectory)s' contains a subordinate directory ('%(emptyDirectory)s') with no files. "
128
+ "Please store the file in the corresponding subfolder or delete the subfolder and upload again."),
129
+ parentDirectory=str(emptyDirectory.parent),
130
+ emptyDirectory=str(emptyDirectory),
131
+ )
110
132
 
111
133
 
112
134
  @validation(
@@ -160,22 +182,13 @@ def rule_EC0130E(
160
182
  EDINET.EC0130E: File extensions must match the file extensions allowed in Figure 2-1-3 and Figure 2-1-5.
161
183
  """
162
184
  uploadContents = pluginData.getUploadContents(fileSource)
163
- checks = []
164
- for instanceType, amendmentPaths in uploadContents.amendmentPaths.items():
165
- for amendmentPath in amendmentPaths:
166
- isSubdirectory = amendmentPath.parent.name != instanceType.value
167
- checks.append((amendmentPath, True, instanceType, isSubdirectory))
168
- for instanceType, formPaths in uploadContents.instances.items():
169
- for amendmentPath in formPaths:
170
- isSubdirectory = amendmentPath.parent.name != instanceType.value
171
- checks.append((amendmentPath, False, instanceType, isSubdirectory))
172
- for path, isAmendment, instanceType, isSubdirectory in checks:
173
- ext = path.suffix
174
- if len(ext) == 0:
185
+ for path, pathInfo in uploadContents.uploadPaths.items():
186
+ if pathInfo.reportFolderType is None or pathInfo.isDirectory:
175
187
  continue
176
- validExtensions = instanceType.getValidExtensions(isAmendment, isSubdirectory)
188
+ validExtensions = pathInfo.reportFolderType.getValidExtensions(pathInfo.isCorrection, pathInfo.isSubdirectory)
177
189
  if validExtensions is None:
178
190
  continue
191
+ ext = path.suffix
179
192
  if ext not in validExtensions:
180
193
  yield Validation.error(
181
194
  codes='EDINET.EC0130E',
@@ -207,18 +220,17 @@ def rule_EC0132E(
207
220
  EDINET.EC0132E: Store the manifest file directly under the relevant folder.
208
221
  """
209
222
  uploadContents = pluginData.getUploadContents(fileSource)
210
- for instanceType in (InstanceType.AUDIT_DOC, InstanceType.PRIVATE_DOC, InstanceType.PUBLIC_DOC):
211
- if instanceType not in uploadContents.instances:
223
+ for reportFolderType, paths in uploadContents.reports.items():
224
+ if reportFolderType.isAttachment:
212
225
  continue
213
- if instanceType.manifestPath in uploadContents.instances.get(instanceType, []):
214
- continue
215
- yield Validation.error(
216
- codes='EDINET.EC0132E',
217
- msg=_("'%(expectedManifestName)s' does not exist in '%(expectedManifestDirectory)s'. "
218
- "Please store the manifest file (or cover file) directly under the relevant folder and upload it again. "),
219
- expectedManifestName=instanceType.manifestPath.name,
220
- expectedManifestDirectory=str(instanceType.manifestPath.parent),
221
- )
226
+ if reportFolderType.manifestPath not in paths:
227
+ yield Validation.error(
228
+ codes='EDINET.EC0132E',
229
+ msg=_("'%(expectedManifestName)s' does not exist in '%(expectedManifestDirectory)s'. "
230
+ "Please store the manifest file (or cover file) directly under the relevant folder and upload it again. "),
231
+ expectedManifestName=reportFolderType.manifestPath.name,
232
+ expectedManifestDirectory=str(reportFolderType.manifestPath.parent),
233
+ )
222
234
 
223
235
 
224
236
  @validation(
@@ -279,6 +291,36 @@ def rule_EC0188E(
279
291
  )
280
292
 
281
293
 
294
+ @validation(
295
+ hook=ValidationHook.FILESOURCE,
296
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
297
+ )
298
+ def rule_EC0192E(
299
+ pluginData: ControllerPluginData,
300
+ cntlr: Cntlr,
301
+ fileSource: FileSource,
302
+ *args: Any,
303
+ **kwargs: Any,
304
+ ) -> Iterable[Validation]:
305
+ """
306
+ EDINET.EC0192E: The cover file for PrivateDoc cannot be set because it uses a
307
+ PublicDoc cover file. Please delete the cover file from PrivateDoc and upload
308
+ it again.
309
+ """
310
+ uploadContents = pluginData.getUploadContents(fileSource)
311
+ for path, pathInfo in uploadContents.uploadPaths.items():
312
+ if not pathInfo.isCoverPage:
313
+ continue
314
+ # Only applies to PrivateDoc correction reports
315
+ if pathInfo.isCorrection and pathInfo.reportFolderType == ReportFolderType.PRIVATE_DOC:
316
+ yield Validation.error(
317
+ codes='EDINET.EC0192E',
318
+ msg=_("The cover file for PrivateDoc ('%(file)s') cannot be set because it uses a PublicDoc cover file. "
319
+ "Please delete the cover file from PrivateDoc and upload it again."),
320
+ file=str(path),
321
+ )
322
+
323
+
282
324
  @validation(
283
325
  hook=ValidationHook.FILESOURCE,
284
326
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -315,6 +357,82 @@ def rule_EC0198E(
315
357
  )
316
358
 
317
359
 
360
+ @validation(
361
+ hook=ValidationHook.FILESOURCE,
362
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
363
+ )
364
+ def rule_EC0233E(
365
+ pluginData: ControllerPluginData,
366
+ cntlr: Cntlr,
367
+ fileSource: FileSource,
368
+ *args: Any,
369
+ **kwargs: Any,
370
+ ) -> Iterable[Validation]:
371
+ """
372
+ EDINET.EC0233E: There is a file in the report directory that comes before the cover file
373
+ in file name sort order.
374
+
375
+ NOTE: This includes files in subdirectories. For example, PublicDoc/00000000_images/image.png
376
+ comes before PublicDoc/0000000_header_*.htm
377
+ """
378
+ uploadContents = pluginData.getUploadContents(fileSource)
379
+ directories = defaultdict(list)
380
+ for path in uploadContents.sortedPaths:
381
+ pathInfo = uploadContents.uploadPaths[path]
382
+ if pathInfo.isDirectory:
383
+ continue
384
+ if pathInfo.reportFolderType in (ReportFolderType.PRIVATE_DOC, ReportFolderType.PUBLIC_DOC):
385
+ directories[pathInfo.reportPath].append(pathInfo)
386
+ for reportPath, pathInfos in directories.items():
387
+ coverPagePath = next(iter(p for p in pathInfos if p.isCoverPage), None)
388
+ if coverPagePath is None:
389
+ continue
390
+ errorPathInfos = pathInfos[:pathInfos.index(coverPagePath)]
391
+ for pathInfo in errorPathInfos:
392
+ yield Validation.error(
393
+ codes='EDINET.EC0233E',
394
+ msg=_("There is a file in the report directory in '%(reportPath)s' that comes before the cover "
395
+ "file ('%(coverPage)s') in file name sort order. "
396
+ "Directory name or file name: '%(path)s'. "
397
+ "Please make sure that there are no files that come before the cover file in the file "
398
+ "name sort order, and then upload again."),
399
+ reportPath=str(reportPath),
400
+ coverPage=str(coverPagePath.path.name),
401
+ path=str(pathInfo.path),
402
+ )
403
+
404
+
405
+ @validation(
406
+ hook=ValidationHook.FILESOURCE,
407
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
408
+ )
409
+ def rule_EC0234E(
410
+ pluginData: ControllerPluginData,
411
+ cntlr: Cntlr,
412
+ fileSource: FileSource,
413
+ *args: Any,
414
+ **kwargs: Any,
415
+ ) -> Iterable[Validation]:
416
+ """
417
+ EDINET.EC0234E: A cover file exists in an unsupported subdirectory.
418
+ """
419
+ uploadContents = pluginData.getUploadContents(fileSource)
420
+ for path, pathInfo in uploadContents.uploadPaths.items():
421
+ if pathInfo.isDirectory:
422
+ continue
423
+ if pathInfo.reportFolderType not in (ReportFolderType.PRIVATE_DOC, ReportFolderType.PUBLIC_DOC):
424
+ continue
425
+ if pathInfo.isSubdirectory and pathInfo.isCoverPage:
426
+ yield Validation.error(
427
+ codes='EDINET.EC0234E',
428
+ msg=_("A cover file ('%(coverPage)s') exists in an unsupported subdirectory. "
429
+ "Directory: '%(directory)s'. "
430
+ "Please make sure there is no cover file in the subfolder and upload again."),
431
+ coverPage=str(path.name),
432
+ directory=str(path.parent),
433
+ )
434
+
435
+
318
436
  @validation(
319
437
  hook=ValidationHook.FILESOURCE,
320
438
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -444,6 +562,61 @@ def rule_EC1020E(
444
562
  )
445
563
 
446
564
 
565
+ @validation(
566
+ hook=ValidationHook.FILESOURCE,
567
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
568
+ )
569
+ def rule_filenames(
570
+ pluginData: ControllerPluginData,
571
+ cntlr: Cntlr,
572
+ fileSource: FileSource,
573
+ *args: Any,
574
+ **kwargs: Any,
575
+ ) -> Iterable[Validation]:
576
+ """
577
+ EDINET.EC0121E: There is a directory or file that contains
578
+ more than 31 characters or uses characters other than those allowed (alphanumeric characters,
579
+ '-' and '_').
580
+ Note: Applies to everything EXCEPT files directly beneath non-correction report folders.
581
+
582
+ EDINET.EC0200E: There is a file that uses characters other
583
+ than those allowed (alphanumeric characters, '-' and '_').
584
+ Note: Applies ONLY to files directly beneath non-correction report folders.
585
+ """
586
+ for path, pathInfo in pluginData.getUploadContents(fileSource).uploadPaths.items():
587
+ isReportFile = (
588
+ not pathInfo.isAttachment and
589
+ not pathInfo.isCorrection and
590
+ not pathInfo.isDirectory and
591
+ not pathInfo.isSubdirectory
592
+ )
593
+ charactersAreValid = FILENAME_STEM_PATTERN.fullmatch(path.stem)
594
+ lengthIsValid = isReportFile or (len(path.name) <= 31)
595
+ if charactersAreValid and lengthIsValid:
596
+ continue
597
+ if isReportFile:
598
+ yield Validation.error(
599
+ codes='EDINET.EC0200E',
600
+ msg=_("There is a file inside the XBRL directory that uses characters "
601
+ "other than those allowed (alphanumeric characters, '-' and '_'). "
602
+ "File: '%(path)s'. "
603
+ "Please change the filename to usable characters, and upload again."),
604
+ path=str(path)
605
+ )
606
+ else:
607
+ yield Validation.error(
608
+ codes='EDINET.EC0121E',
609
+ msg=_("There is a directory or file in '%(directory)s' that contains more "
610
+ "than 31 characters or uses characters other than those allowed "
611
+ "(alphanumeric characters, '-' and '_'). "
612
+ "Directory or filename: '%(basename)s'. "
613
+ "Please change the file name (or folder name) to within 31 characters and to usable "
614
+ "characters, and upload again."),
615
+ directory=str(path.parent),
616
+ basename=path.name,
617
+ )
618
+
619
+
447
620
  @validation(
448
621
  hook=ValidationHook.FILESOURCE,
449
622
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arelle-release
3
- Version: 2.37.50
3
+ Version: 2.37.51
4
4
  Summary: An open source XBRL platform.
5
5
  Author-email: "arelle.org" <support@arelle.org>
6
6
  License-Expression: Apache-2.0
@@ -125,7 +125,7 @@ arelle/XmlValidateConst.py,sha256=U_wN0Q-nWKwf6dKJtcu_83FXPn9c6P8JjzGA5b0w7P0,33
125
125
  arelle/XmlValidateParticles.py,sha256=Mn6vhFl0ZKC_vag1mBwn1rH_x2jmlusJYqOOuxFPO2k,9231
126
126
  arelle/XmlValidateSchema.py,sha256=6frtZOc1Yrx_5yYF6V6oHbScnglWrVbWr6xW4EHtLQI,7428
127
127
  arelle/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
128
- arelle/_version.py,sha256=njDexQGIu5W1_Bx2XXI0APU6EFVbL-MHc7OHXGD6xDg,515
128
+ arelle/_version.py,sha256=5ledvoDL_7HieaAt4kjZqd9TGIaAC8kJJgJuFA-vUT4,708
129
129
  arelle/typing.py,sha256=PRe-Fxwr2SBqYYUVPCJ3E7ddDX0_oOISNdT5Q97EbRM,1246
130
130
  arelle/api/Session.py,sha256=kgSxS7VckA1sQ7xp0pJiK7IK-vRxAdAZKUo8gEx27s8,7549
131
131
  arelle/config/creationSoftwareNames.json,sha256=5MK7XUjfDJ9OpRCCHXeOErJ1SlTBZji4WEcEOdOacx0,3128
@@ -312,14 +312,14 @@ arelle/plugin/validate/DBA/rules/tm.py,sha256=ui9oKBqlAForwkQ9kk9KBiUogTJE5pv1Rb
312
312
  arelle/plugin/validate/DBA/rules/tr.py,sha256=4TootFjl0HXsKZk1XNBCyj-vnjRs4lg35hfiz_b_4wU,14684
313
313
  arelle/plugin/validate/EBA/__init__.py,sha256=x3zXNcdSDJ3kHfL7kMs0Ve0Vs9oWbzNFVf1TK4Avmy8,45924
314
314
  arelle/plugin/validate/EBA/config.xml,sha256=37wMVUAObK-XEqakqD8zPNog20emYt4a_yfL1AKubF8,2022
315
- arelle/plugin/validate/EDINET/Constants.py,sha256=bYLMdzEFw6X1jchW5iN5pUAQ8K7-cGtiZRN22r1LP6g,1339
316
- arelle/plugin/validate/EDINET/ControllerPluginData.py,sha256=PrEhllue_lh-sLeUy9w8I9XLMLgpo5eW-c7ZKgNyGLE,6256
315
+ arelle/plugin/validate/EDINET/Constants.py,sha256=H_OX8hq7nns6TbKPpmDlg1_pNOd7P-XMFF03MMZBAuk,1387
316
+ arelle/plugin/validate/EDINET/ControllerPluginData.py,sha256=T4m8GFiVg9tNoLcC3HxeegNnKVqAqZdc6kNdxM5Vz1Y,7005
317
317
  arelle/plugin/validate/EDINET/DisclosureSystems.py,sha256=3rKG42Eg-17Xx_KXU_V5yHW6I3LTwQunvf4a44C9k_4,36
318
- arelle/plugin/validate/EDINET/InstanceType.py,sha256=aLKb4-AJ6nDZKMOLCp7u08E9VD64ExeZy9_oGth-LTk,3207
319
318
  arelle/plugin/validate/EDINET/ManifestInstance.py,sha256=SkQV-aOsYn3CTgCkH4IXNdM3QKoiz8okwb29ftMtV3Q,6882
320
- arelle/plugin/validate/EDINET/PluginValidationDataExtension.py,sha256=NBCXANYep0AvQVe2zm2eaKzklvLCC1zeQ_oa8L6cryM,11498
319
+ arelle/plugin/validate/EDINET/PluginValidationDataExtension.py,sha256=4xGDhwLo_JOJSu16w_CkPCca1SYMu980bb2-r6sbI5s,12459
320
+ arelle/plugin/validate/EDINET/ReportFolderType.py,sha256=Q-9a-5tJfhK-cjY8WUB2AT1NI-Nn9cFtARVOIJoLRGU,2979
321
321
  arelle/plugin/validate/EDINET/Statement.py,sha256=0Mw5IB7LMtvUZ-2xKZfxmq67xF_dCgJo3eNLweLFRHU,9350
322
- arelle/plugin/validate/EDINET/UploadContents.py,sha256=L0u5171cLBKX7NT-_szRqOfkiy4Gv1xPsfpPVgPhtu0,409
322
+ arelle/plugin/validate/EDINET/UploadContents.py,sha256=IKQYl6lXYTfAZKLIDzRRGSRt3FXoL2Eldbx3Dh7T2I4,712
323
323
  arelle/plugin/validate/EDINET/ValidationPluginExtension.py,sha256=oMY0ntLr1qIh3uMi1W_M-bT5bhXPDx048X3oDFP5zOY,2042
324
324
  arelle/plugin/validate/EDINET/__init__.py,sha256=OZ7gMknCHd0M-9nt8UOmjEZW50YQzbvSLongr9O7Yi0,3022
325
325
  arelle/plugin/validate/EDINET/resources/config.xml,sha256=7uT4GcRgk5veMLpFhPPQJxbGKiQvM52P8EMrjn0qd0g,646
@@ -328,9 +328,9 @@ arelle/plugin/validate/EDINET/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQ
328
328
  arelle/plugin/validate/EDINET/rules/contexts.py,sha256=KPoyWfRaURvxoGVcWP64mTMTAKPMSmQSX06RClCLddw,7590
329
329
  arelle/plugin/validate/EDINET/rules/edinet.py,sha256=VYrDZaKbsQuQEvOY5F0Pv4Jzk9YZ4iETOkAFOggrhEY,12632
330
330
  arelle/plugin/validate/EDINET/rules/frta.py,sha256=N0YglHYZuLD2IuwE26viR2ViwUYjneBuMFU9vlrS0aQ,7616
331
- arelle/plugin/validate/EDINET/rules/gfm.py,sha256=Ty5zDqIRkXwnOQTzK5JD2F6jbK1xEiseltosVfYUrVM,24772
331
+ arelle/plugin/validate/EDINET/rules/gfm.py,sha256=vJz8OGbXQb1Lbp5tF6ObCifM3AuPB_PXTjSPU5WXYF4,23484
332
332
  arelle/plugin/validate/EDINET/rules/manifests.py,sha256=MoT9R_a4BzuYdQVbF7RC5wz134Ve68svSdJ3NlpO_AU,4026
333
- arelle/plugin/validate/EDINET/rules/upload.py,sha256=k1o12K_vMN2N5bAXPxLRwyKjghoOGrgfLijE_j_5ilQ,19811
333
+ arelle/plugin/validate/EDINET/rules/upload.py,sha256=VQKjuDxMAER14Cb0kymoc-xdnAqJ9l1ZaqlQNIEG8gU,26606
334
334
  arelle/plugin/validate/ESEF/Const.py,sha256=JujF_XV-_TNsxjGbF-8SQS4OOZIcJ8zhCMnr-C1O5Ho,22660
335
335
  arelle/plugin/validate/ESEF/Dimensions.py,sha256=MOJM7vwNPEmV5cu-ZzPrhx3347ZvxgD6643OB2HRnIk,10597
336
336
  arelle/plugin/validate/ESEF/Util.py,sha256=QH3btcGqBpr42M7WSKZLSdNXygZaZLfEiEjlxoG21jE,7950
@@ -680,9 +680,9 @@ arelle/utils/validate/ValidationUtil.py,sha256=9vmSvShn-EdQy56dfesyV8JjSRVPj7txr
680
680
  arelle/utils/validate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
681
681
  arelle/webserver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
682
682
  arelle/webserver/bottle.py,sha256=P-JECd9MCTNcxCnKoDUvGcoi03ezYVOgoWgv2_uH-6M,362
683
- arelle_release-2.37.50.dist-info/licenses/LICENSE.md,sha256=Q0tn6q0VUbr-NM8916513NCIG8MNzo24Ev-sxMUBRZc,3959
684
- arelle_release-2.37.50.dist-info/METADATA,sha256=4w922-d1vIeCmfCpQ18ImtcI_0C7_qguifO2WlU-yYo,9327
685
- arelle_release-2.37.50.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
686
- arelle_release-2.37.50.dist-info/entry_points.txt,sha256=Uj5niwfwVsx3vaQ3fYj8hrZ1xpfCJyTUA09tYKWbzpo,111
687
- arelle_release-2.37.50.dist-info/top_level.txt,sha256=fwU7SYawL4_r-sUMRg7r1nYVGjFMSDvRWx8VGAXEw7w,7
688
- arelle_release-2.37.50.dist-info/RECORD,,
683
+ arelle_release-2.37.51.dist-info/licenses/LICENSE.md,sha256=Q0tn6q0VUbr-NM8916513NCIG8MNzo24Ev-sxMUBRZc,3959
684
+ arelle_release-2.37.51.dist-info/METADATA,sha256=BZlHz_pXjybuHYVwQ-qzY5TmwxoyNVIRhJTjBez0H6w,9327
685
+ arelle_release-2.37.51.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
686
+ arelle_release-2.37.51.dist-info/entry_points.txt,sha256=Uj5niwfwVsx3vaQ3fYj8hrZ1xpfCJyTUA09tYKWbzpo,111
687
+ arelle_release-2.37.51.dist-info/top_level.txt,sha256=fwU7SYawL4_r-sUMRg7r1nYVGjFMSDvRWx8VGAXEw7w,7
688
+ arelle_release-2.37.51.dist-info/RECORD,,