cpp-xer-parser 0.6.1__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.
- cpp_xer_parser/__init__.py +38 -0
- cpp_xer_parser/config_profiles.py +131 -0
- cpp_xer_parser/validation.py +164 -0
- cpp_xer_parser/xer_parser.py +3468 -0
- cpp_xer_parser-0.6.1.dist-info/METADATA +287 -0
- cpp_xer_parser-0.6.1.dist-info/RECORD +10 -0
- cpp_xer_parser-0.6.1.dist-info/WHEEL +5 -0
- cpp_xer_parser-0.6.1.dist-info/entry_points.txt +2 -0
- cpp_xer_parser-0.6.1.dist-info/licenses/LICENSE +21 -0
- cpp_xer_parser-0.6.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""cpp-xer-parser, installed: ``pip install cpp-xer-parser``.
|
|
2
|
+
|
|
3
|
+
The package is the repository's scripts/ directory. Everything the parser
|
|
4
|
+
offers is in ``cpp_xer_parser.xer_parser``; the names below are the ones
|
|
5
|
+
most callers need::
|
|
6
|
+
|
|
7
|
+
from cpp_xer_parser import parse_xer, get_table, print_summary
|
|
8
|
+
|
|
9
|
+
A plain clone does not import this file: it puts scripts/ on sys.path and
|
|
10
|
+
imports ``xer_parser`` directly, as the README's quick start shows.
|
|
11
|
+
"""
|
|
12
|
+
from .xer_parser import ( # noqa: F401
|
|
13
|
+
build_activity_code_map,
|
|
14
|
+
build_predecessor_map,
|
|
15
|
+
build_resource_map,
|
|
16
|
+
build_udf_map,
|
|
17
|
+
build_wbs_map,
|
|
18
|
+
compute_half_step_xer,
|
|
19
|
+
generate_summary,
|
|
20
|
+
generate_xer,
|
|
21
|
+
get_calendar_map,
|
|
22
|
+
get_fields,
|
|
23
|
+
get_table,
|
|
24
|
+
get_work_days_between,
|
|
25
|
+
parse_xer,
|
|
26
|
+
print_summary,
|
|
27
|
+
validate_schedule,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = '0.6.1'
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
'parse_xer', 'get_table', 'get_fields', 'get_calendar_map',
|
|
34
|
+
'get_work_days_between', 'build_wbs_map', 'build_resource_map',
|
|
35
|
+
'build_predecessor_map', 'build_activity_code_map', 'build_udf_map',
|
|
36
|
+
'generate_summary', 'print_summary', 'generate_xer',
|
|
37
|
+
'compute_half_step_xer', 'validate_schedule', '__version__',
|
|
38
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
config_profiles.py — minimal DCMA-14 / cp_validator threshold profiles
|
|
3
|
+
|
|
4
|
+
Public, standalone subset of the profile registry used by cp_validator and
|
|
5
|
+
dcma14. The full version lives inside the Critical Path Partners internal
|
|
6
|
+
`_cpp_common` module and additionally drives forensic-delay-analysis,
|
|
7
|
+
schedule-risk-analysis, and time-impact-analysis. This subset covers the
|
|
8
|
+
threshold keys consumed by the DCMA-14 check suite.
|
|
9
|
+
|
|
10
|
+
Three profiles ship out of the box:
|
|
11
|
+
- `commercial` — generic commercial-construction defaults
|
|
12
|
+
- `nuclear` — tightened thresholds suitable for nuclear / heavy energy
|
|
13
|
+
where regulator scrutiny is high
|
|
14
|
+
- `mining` — slightly relaxed thresholds for resource-driven mining
|
|
15
|
+
schedules where some DCMA assumptions don't translate
|
|
16
|
+
|
|
17
|
+
Callers pass a profile by name (e.g. the `profile` parameter on
|
|
18
|
+
`dcma_14_assess`). `get_profile` looks the name up in `_PROFILES`, so an
|
|
19
|
+
unknown name raises ValueError and a dict raises TypeError. It returns a copy,
|
|
20
|
+
which callers may read or change without affecting the registry. To run other
|
|
21
|
+
thresholds, add an entry to `_PROFILES` with every key the bundled profiles
|
|
22
|
+
carry, and pass its name.
|
|
23
|
+
|
|
24
|
+
The keys of the commercial profile below are commented with the DCMA 14-Point
|
|
25
|
+
criterion each one sets, in the published numbering; the `_check_NN_*`
|
|
26
|
+
functions in dcma14.py read them by name.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from typing import Dict
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
33
|
+
# Profile registry
|
|
34
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
_COMMERCIAL: Dict[str, object] = {
|
|
37
|
+
'name': 'Commercial Construction',
|
|
38
|
+
# DCMA 14-Point thresholds
|
|
39
|
+
'dcma_logic_max_missing_pct': 5.0, # #1 Logic
|
|
40
|
+
'dcma_leads_max_count': 0, # #2 Leads (negative lags)
|
|
41
|
+
'dcma_lags_max_pct': 5.0, # #3 Lags
|
|
42
|
+
'dcma_fs_min_pct': 90.0, # #4 FS Relationship %
|
|
43
|
+
'dcma_hard_constraints_max_pct': 5.0, # #5 Hard Constraints
|
|
44
|
+
'dcma_high_float_max_days': 44, # #6 High Float threshold
|
|
45
|
+
'dcma_high_float_max_pct': 5.0,
|
|
46
|
+
'dcma_negative_float_max_count': 0, # #7 Negative Float
|
|
47
|
+
'dcma_high_duration_max_days': 44, # #8 High Duration threshold
|
|
48
|
+
'dcma_high_duration_max_pct': 5.0,
|
|
49
|
+
'dcma_invalid_dates_max_count': 0, # #9 Invalid Dates / Future Actuals
|
|
50
|
+
'dcma_resources_min_pct': 100.0, # #10 Resources: every activity of 1+ working day resourced, on every profile (the written DCMA definition)
|
|
51
|
+
'dcma_missed_tasks_max_pct': 5.0, # #11 Missed Tasks
|
|
52
|
+
'dcma_cpli_min': 0.95, # #13 CPLI
|
|
53
|
+
'bei_min': 0.95, # #14 BEI
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_NUCLEAR: Dict[str, object] = {
|
|
57
|
+
'name': 'Nuclear / Heavy Energy',
|
|
58
|
+
'dcma_logic_max_missing_pct': 1.0,
|
|
59
|
+
'dcma_leads_max_count': 0,
|
|
60
|
+
'dcma_lags_max_pct': 2.0,
|
|
61
|
+
'dcma_fs_min_pct': 95.0,
|
|
62
|
+
'dcma_hard_constraints_max_pct': 2.0,
|
|
63
|
+
'dcma_high_float_max_days': 30,
|
|
64
|
+
'dcma_high_float_max_pct': 3.0,
|
|
65
|
+
'dcma_negative_float_max_count': 0,
|
|
66
|
+
'dcma_high_duration_max_days': 20,
|
|
67
|
+
'dcma_high_duration_max_pct': 3.0,
|
|
68
|
+
'dcma_invalid_dates_max_count': 0,
|
|
69
|
+
'dcma_resources_min_pct': 100.0,
|
|
70
|
+
'dcma_missed_tasks_max_pct': 3.0,
|
|
71
|
+
'dcma_cpli_min': 0.98,
|
|
72
|
+
'bei_min': 0.98,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_MINING: Dict[str, object] = {
|
|
76
|
+
'name': 'Mining / Resource-Driven',
|
|
77
|
+
'dcma_logic_max_missing_pct': 5.0,
|
|
78
|
+
'dcma_leads_max_count': 0,
|
|
79
|
+
'dcma_lags_max_pct': 10.0,
|
|
80
|
+
'dcma_fs_min_pct': 85.0,
|
|
81
|
+
'dcma_hard_constraints_max_pct': 8.0,
|
|
82
|
+
'dcma_high_float_max_days': 90,
|
|
83
|
+
'dcma_high_float_max_pct': 10.0,
|
|
84
|
+
'dcma_negative_float_max_count': 0,
|
|
85
|
+
'dcma_high_duration_max_days': 90,
|
|
86
|
+
'dcma_high_duration_max_pct': 10.0,
|
|
87
|
+
'dcma_invalid_dates_max_count': 0,
|
|
88
|
+
'dcma_resources_min_pct': 100.0,
|
|
89
|
+
'dcma_missed_tasks_max_pct': 5.0,
|
|
90
|
+
'dcma_cpli_min': 0.93,
|
|
91
|
+
'bei_min': 0.92,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_PROFILES: Dict[str, Dict[str, object]] = {
|
|
95
|
+
'commercial': _COMMERCIAL,
|
|
96
|
+
'nuclear': _NUCLEAR,
|
|
97
|
+
'mining': _MINING,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_profile(name: str) -> Dict[str, object]:
|
|
102
|
+
"""Return the threshold dict for a named profile.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
name : 'commercial' | 'nuclear' | 'mining'
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
dict
|
|
111
|
+
A shallow copy of the registered profile. Callers may mutate the
|
|
112
|
+
return value without affecting the registry.
|
|
113
|
+
|
|
114
|
+
Raises
|
|
115
|
+
------
|
|
116
|
+
ValueError
|
|
117
|
+
If `name` is not a registered profile.
|
|
118
|
+
"""
|
|
119
|
+
if name not in _PROFILES:
|
|
120
|
+
valid = sorted(_PROFILES)
|
|
121
|
+
raise ValueError(f"Unknown profile {name!r}; valid: {valid}")
|
|
122
|
+
# Return a copy so callers can safely mutate.
|
|
123
|
+
return dict(_PROFILES[name])
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def list_profiles() -> Dict[str, str]:
|
|
127
|
+
"""Return {profile_id: human_name} over all registered profiles."""
|
|
128
|
+
return {pid: prof.get('name', pid) for pid, prof in _PROFILES.items()}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = ['get_profile', 'list_profiles']
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""
|
|
2
|
+
validation.py — minimal Finding / ValidationReport types
|
|
3
|
+
|
|
4
|
+
Public, standalone subset of the validation primitives used by cp_validator
|
|
5
|
+
and dcma14. The full version lives inside the Critical Path Partners
|
|
6
|
+
internal `_cpp_common` module and carries additional plumbing for the
|
|
7
|
+
forensic-suite-wide audit trail. This subset is sufficient for the OSS
|
|
8
|
+
critical-path-validator, and for `xer_parser.validate_schedule` and
|
|
9
|
+
`xer_parser.aace_31r_compliance`, to run end-to-end from a plain clone.
|
|
10
|
+
|
|
11
|
+
The four severity sentinels (BLOCK / WARN / INFO / PASS) are plain strings —
|
|
12
|
+
matching the public-API contract of the full version — so any code that
|
|
13
|
+
imports them, compares against them, or includes them in serialized output
|
|
14
|
+
will behave identically.
|
|
15
|
+
|
|
16
|
+
`ValidationReport.to_dict()` carries the full version's `summary` block: the
|
|
17
|
+
number of findings at each severity, the total, and `worst_severity`, the
|
|
18
|
+
worst severity present (BLOCK > WARN > INFO > PASS; PASS for an empty
|
|
19
|
+
report). A caller that reads `to_dict()['summary']['worst_severity']` gets
|
|
20
|
+
the same answer from either version. Two differences remain, both kept so
|
|
21
|
+
that output this subset already produced does not move: `to_dict()` also
|
|
22
|
+
carries this subset's `counts` key, and it lists findings in the order they
|
|
23
|
+
were added, where the full version sorts them worst first.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import Any, Dict, List, Optional
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
31
|
+
# Severity sentinels
|
|
32
|
+
# Public-API contract: these are strings. Compare with ==, not `is`.
|
|
33
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
34
|
+
BLOCK = 'BLOCK' # Forensic-correctness blocker. Findings at this level fail the suite.
|
|
35
|
+
WARN = 'WARN' # Caution-level finding. The schedule may still be usable.
|
|
36
|
+
INFO = 'INFO' # Informational note. No corrective action implied.
|
|
37
|
+
PASS = 'PASS' # Check passed. No finding to report.
|
|
38
|
+
|
|
39
|
+
_VALID_SEVERITIES = (BLOCK, WARN, INFO, PASS)
|
|
40
|
+
|
|
41
|
+
# Rank of each severity, worst highest. `ValidationReport.worst_severity`
|
|
42
|
+
# reads the worst finding by this order.
|
|
43
|
+
_SEVERITY_ORDER = {BLOCK: 3, WARN: 2, INFO: 1, PASS: 0}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Finding:
|
|
48
|
+
"""A single validation finding produced by a check.
|
|
49
|
+
|
|
50
|
+
Attributes
|
|
51
|
+
----------
|
|
52
|
+
severity : one of BLOCK / WARN / INFO / PASS
|
|
53
|
+
check_id : stable identifier for the check (e.g. 'DCMA-01-Logic')
|
|
54
|
+
message : human-readable description
|
|
55
|
+
reference : citation string (e.g. 'DCMA 14-Point #1', 'AACE 49R-06')
|
|
56
|
+
evidence : structured supporting data (key/value, e.g. {'value': ..., 'threshold': ...})
|
|
57
|
+
"""
|
|
58
|
+
severity: str
|
|
59
|
+
check_id: str
|
|
60
|
+
message: str
|
|
61
|
+
reference: str = ''
|
|
62
|
+
evidence: Dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
def __post_init__(self):
|
|
65
|
+
if self.severity not in _VALID_SEVERITIES:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Finding.severity must be one of {_VALID_SEVERITIES}; got {self.severity!r}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
71
|
+
return {
|
|
72
|
+
'severity': self.severity,
|
|
73
|
+
'check_id': self.check_id,
|
|
74
|
+
'message': self.message,
|
|
75
|
+
'reference': self.reference,
|
|
76
|
+
'evidence': dict(self.evidence),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class ValidationReport:
|
|
82
|
+
"""An ordered list of Finding instances plus a subject and context.
|
|
83
|
+
|
|
84
|
+
Attributes
|
|
85
|
+
----------
|
|
86
|
+
subject : free-form report title (e.g. 'DCMA 14-Point Assessment [commercial]')
|
|
87
|
+
context : free-form metadata (profile name, schedule identifiers, etc.)
|
|
88
|
+
findings: ordered list of Finding instances (insertion order preserved)
|
|
89
|
+
"""
|
|
90
|
+
subject: str = ''
|
|
91
|
+
context: Dict[str, Any] = field(default_factory=dict)
|
|
92
|
+
findings: List[Finding] = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
def add(self, finding: Finding) -> None:
|
|
95
|
+
"""Append a Finding to the report."""
|
|
96
|
+
if not isinstance(finding, Finding):
|
|
97
|
+
raise TypeError(f"add() expects a Finding instance; got {type(finding).__name__}")
|
|
98
|
+
self.findings.append(finding)
|
|
99
|
+
|
|
100
|
+
def by_severity(self, severity: str) -> List[Finding]:
|
|
101
|
+
"""Return findings filtered to one severity level."""
|
|
102
|
+
return [f for f in self.findings if f.severity == severity]
|
|
103
|
+
|
|
104
|
+
def count(self, severity=None):
|
|
105
|
+
"""Return the number of findings at one severity level, or the
|
|
106
|
+
number of all findings when no severity is given.
|
|
107
|
+
|
|
108
|
+
Carried alongside `counts()` because callers use both shapes:
|
|
109
|
+
`xer_parser.aace_31r_compliance` scores a schedule by calling
|
|
110
|
+
`count(BLOCK)` and `count(WARN)` directly, so a subset without this
|
|
111
|
+
method makes that function raise AttributeError on a plain clone.
|
|
112
|
+
`count()` with no argument is the `total` in `to_dict()['summary']`,
|
|
113
|
+
as in the full version.
|
|
114
|
+
"""
|
|
115
|
+
if severity is None:
|
|
116
|
+
return len(self.findings)
|
|
117
|
+
return sum(1 for f in self.findings if f.severity == severity)
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def worst_severity(self):
|
|
121
|
+
"""Highest severity present, or PASS if none."""
|
|
122
|
+
if not self.findings:
|
|
123
|
+
return PASS
|
|
124
|
+
return max(self.findings, key=lambda f: _SEVERITY_ORDER[f.severity]).severity
|
|
125
|
+
|
|
126
|
+
def counts(self) -> Dict[str, int]:
|
|
127
|
+
"""Return a dict of severity → count over all findings."""
|
|
128
|
+
out = {sev: 0 for sev in _VALID_SEVERITIES}
|
|
129
|
+
for f in self.findings:
|
|
130
|
+
out[f.severity] = out.get(f.severity, 0) + 1
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
def has_blocking(self) -> bool:
|
|
134
|
+
"""True if any finding is BLOCK severity."""
|
|
135
|
+
return any(f.severity == BLOCK for f in self.findings)
|
|
136
|
+
|
|
137
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
138
|
+
"""Return the report as a JSON-safe dict.
|
|
139
|
+
|
|
140
|
+
`summary` has the full version's keys and values: the count at each
|
|
141
|
+
severity, `total`, and `worst_severity`. `findings` are in the order
|
|
142
|
+
they were added (the full version sorts them worst first), and
|
|
143
|
+
`counts` is kept for callers of earlier releases.
|
|
144
|
+
"""
|
|
145
|
+
return {
|
|
146
|
+
'subject': self.subject,
|
|
147
|
+
'context': dict(self.context),
|
|
148
|
+
'summary': {
|
|
149
|
+
'block': self.count(BLOCK),
|
|
150
|
+
'warn': self.count(WARN),
|
|
151
|
+
'info': self.count(INFO),
|
|
152
|
+
'pass': self.count(PASS),
|
|
153
|
+
'total': self.count(),
|
|
154
|
+
'worst_severity': self.worst_severity,
|
|
155
|
+
},
|
|
156
|
+
'findings': [f.to_dict() for f in self.findings],
|
|
157
|
+
'counts': self.counts(),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
__all__ = [
|
|
162
|
+
'BLOCK', 'WARN', 'INFO', 'PASS',
|
|
163
|
+
'Finding', 'ValidationReport',
|
|
164
|
+
]
|