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.
@@ -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
+ ]