pycfdp 0.2.2__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.
- cfdp/__init__.py +106 -0
- cfdp/checksum.py +94 -0
- cfdp/constants.py +153 -0
- cfdp/core.py +895 -0
- cfdp/event.py +55 -0
- cfdp/filestore/__init__.py +2 -0
- cfdp/filestore/base.py +99 -0
- cfdp/filestore/native.py +162 -0
- cfdp/meta/__init__.py +3 -0
- cfdp/meta/datafield.py +11 -0
- cfdp/meta/filestore.py +127 -0
- cfdp/meta/message.py +659 -0
- cfdp/pdu/__init__.py +8 -0
- cfdp/pdu/ack.py +90 -0
- cfdp/pdu/eof.py +123 -0
- cfdp/pdu/filedata.py +90 -0
- cfdp/pdu/finished.py +99 -0
- cfdp/pdu/header.py +159 -0
- cfdp/pdu/keep_alive.py +108 -0
- cfdp/pdu/metadata.py +211 -0
- cfdp/pdu/nak.py +220 -0
- cfdp/pdu/prompt.py +75 -0
- cfdp/pure/__init__.py +68 -0
- cfdp/pure/config.py +141 -0
- cfdp/pure/indications.py +130 -0
- cfdp/pure/machines/__init__.py +25 -0
- cfdp/pure/machines/receiver1.py +433 -0
- cfdp/pure/machines/receiver2.py +893 -0
- cfdp/pure/machines/sender1.py +400 -0
- cfdp/pure/machines/sender2.py +774 -0
- cfdp/pure/outputs.py +203 -0
- cfdp/pure/query_port.py +48 -0
- cfdp/pure/transaction.py +43 -0
- cfdp/py.typed +0 -0
- cfdp/remote_entity_config.py +12 -0
- cfdp/shell/__init__.py +44 -0
- cfdp/shell/checksum_service.py +136 -0
- cfdp/shell/dispatcher.py +185 -0
- cfdp/shell/effect_executor.py +285 -0
- cfdp/shell/machine_registry.py +165 -0
- cfdp/shell/query_port.py +70 -0
- cfdp/shell/timer_service.py +252 -0
- cfdp/transaction_handle.py +50 -0
- cfdp/transport/__init__.py +0 -0
- cfdp/transport/base.py +12 -0
- cfdp/transport/spacepacket.py +47 -0
- cfdp/transport/udp.py +90 -0
- cfdp/transport/zmq.py +54 -0
- pycfdp-0.2.2.dist-info/METADATA +76 -0
- pycfdp-0.2.2.dist-info/RECORD +53 -0
- pycfdp-0.2.2.dist-info/WHEEL +5 -0
- pycfdp-0.2.2.dist-info/licenses/LICENSE.txt +19 -0
- pycfdp-0.2.2.dist-info/top_level.txt +1 -0
cfdp/pure/config.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Per-transaction configuration snapshot (design D9).
|
|
2
|
+
|
|
3
|
+
``TransactionConfig`` is a frozen snapshot that merges every per-transaction
|
|
4
|
+
knob **once** at machine-creation time with the precedence
|
|
5
|
+
|
|
6
|
+
entity defaults <- remote-entity config <- per-``put`` overrides
|
|
7
|
+
|
|
8
|
+
and folds in the resolved fault-handler map. ``get_fault_handler`` is then a
|
|
9
|
+
pure dict lookup — no ``kernel`` access — which removes the last reason a
|
|
10
|
+
machine or ``Transaction`` needs a ``kernel`` reference.
|
|
11
|
+
|
|
12
|
+
Three plain input layers are modelled here so the merge is testable in
|
|
13
|
+
isolation and the shell (issue #16) can build them without leaking itself:
|
|
14
|
+
|
|
15
|
+
* :class:`EntityDefaults` — the entity-level knobs (always concrete values).
|
|
16
|
+
* :class:`~cfdp.remote_entity_config.RemoteEntityConfig` — optional per-peer
|
|
17
|
+
overrides (existing type; ``None`` fields mean "no opinion").
|
|
18
|
+
* :class:`PutOverrides` — optional per-``put`` overrides (``None`` fields mean
|
|
19
|
+
"no opinion").
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from ..constants import DEFAULT_FAULT_HANDLERS
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class EntityDefaults:
|
|
30
|
+
"""Entity-level knob values — the lowest-precedence layer."""
|
|
31
|
+
|
|
32
|
+
transaction_inactivity_limit: float
|
|
33
|
+
ack_timer_interval: float
|
|
34
|
+
ack_timer_expiration_limit: int
|
|
35
|
+
nak_timer_interval: float
|
|
36
|
+
nak_timer_expiration_limit: int
|
|
37
|
+
maximum_file_segment_length: int
|
|
38
|
+
inter_pdu_delay: float = 0.0
|
|
39
|
+
# The entity's default fault-handler map. Codes it omits still fall back to
|
|
40
|
+
# DEFAULT_FAULT_HANDLERS during resolution.
|
|
41
|
+
fault_handlers: dict[Any, Any] = field(
|
|
42
|
+
default_factory=lambda: dict(DEFAULT_FAULT_HANDLERS)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class PutOverrides:
|
|
48
|
+
"""Per-``put`` knob overrides — the highest-precedence layer.
|
|
49
|
+
|
|
50
|
+
Every field defaults to ``None`` meaning "defer to the lower layers".
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
transaction_inactivity_limit: float | None = None
|
|
54
|
+
ack_timer_interval: float | None = None
|
|
55
|
+
ack_timer_expiration_limit: int | None = None
|
|
56
|
+
nak_timer_interval: float | None = None
|
|
57
|
+
nak_timer_expiration_limit: int | None = None
|
|
58
|
+
maximum_file_segment_length: int | None = None
|
|
59
|
+
inter_pdu_delay: float | None = None
|
|
60
|
+
fault_handler_overrides: dict[Any, Any] | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _pick(*candidates_high_to_low):
|
|
64
|
+
"""Return the first non-``None`` candidate (highest precedence first)."""
|
|
65
|
+
for value in candidates_high_to_low:
|
|
66
|
+
if value is not None:
|
|
67
|
+
return value
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class TransactionConfig:
|
|
73
|
+
"""Frozen, fully-resolved per-transaction configuration."""
|
|
74
|
+
|
|
75
|
+
transaction_inactivity_limit: float
|
|
76
|
+
ack_timer_interval: float
|
|
77
|
+
ack_timer_expiration_limit: int
|
|
78
|
+
nak_timer_interval: float
|
|
79
|
+
nak_timer_expiration_limit: int
|
|
80
|
+
maximum_file_segment_length: int
|
|
81
|
+
inter_pdu_delay: float
|
|
82
|
+
fault_handlers: dict[Any, Any]
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def resolve(cls, entity_defaults, remote_config=None, put_overrides=None):
|
|
86
|
+
"""Merge the three layers into an immutable snapshot.
|
|
87
|
+
|
|
88
|
+
Precedence for every scalar knob: ``put_overrides`` (if not ``None``)
|
|
89
|
+
beats ``remote_config`` (if not ``None``) beats ``entity_defaults``.
|
|
90
|
+
"""
|
|
91
|
+
e = entity_defaults
|
|
92
|
+
r = remote_config
|
|
93
|
+
p = put_overrides
|
|
94
|
+
|
|
95
|
+
def pick(name, entity_value, remote_value=_UNSET):
|
|
96
|
+
put_value = getattr(p, name, None) if p is not None else None
|
|
97
|
+
if remote_value is _UNSET:
|
|
98
|
+
remote_value = getattr(r, name, None) if r is not None else None
|
|
99
|
+
return _pick(put_value, remote_value, entity_value)
|
|
100
|
+
|
|
101
|
+
# RemoteEntityConfig has no maximum_file_segment_length field, so its
|
|
102
|
+
# remote-layer value is always absent (None).
|
|
103
|
+
maximum_file_segment_length = _pick(
|
|
104
|
+
p.maximum_file_segment_length if p is not None else None,
|
|
105
|
+
e.maximum_file_segment_length,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return cls(
|
|
109
|
+
transaction_inactivity_limit=pick(
|
|
110
|
+
"transaction_inactivity_limit", e.transaction_inactivity_limit
|
|
111
|
+
),
|
|
112
|
+
ack_timer_interval=pick("ack_timer_interval", e.ack_timer_interval),
|
|
113
|
+
ack_timer_expiration_limit=pick(
|
|
114
|
+
"ack_timer_expiration_limit", e.ack_timer_expiration_limit
|
|
115
|
+
),
|
|
116
|
+
nak_timer_interval=pick("nak_timer_interval", e.nak_timer_interval),
|
|
117
|
+
nak_timer_expiration_limit=pick(
|
|
118
|
+
"nak_timer_expiration_limit", e.nak_timer_expiration_limit
|
|
119
|
+
),
|
|
120
|
+
maximum_file_segment_length=maximum_file_segment_length,
|
|
121
|
+
inter_pdu_delay=pick("inter_pdu_delay", e.inter_pdu_delay),
|
|
122
|
+
fault_handlers=_resolve_fault_handlers(e, r, p),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def get_fault_handler(self, fault):
|
|
126
|
+
"""Pure lookup of the resolved fault-handler action for ``fault``."""
|
|
127
|
+
return self.fault_handlers[fault]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
_UNSET = object()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _resolve_fault_handlers(entity_defaults, remote_config, put_overrides):
|
|
134
|
+
"""Layer the fault-handler maps: spec default <- entity <- remote <- put."""
|
|
135
|
+
merged = dict(DEFAULT_FAULT_HANDLERS)
|
|
136
|
+
merged.update(entity_defaults.fault_handlers or {})
|
|
137
|
+
if remote_config is not None and remote_config.default_fault_handlers is not None:
|
|
138
|
+
merged.update(remote_config.default_fault_handlers)
|
|
139
|
+
if put_overrides is not None and put_overrides.fault_handler_overrides is not None:
|
|
140
|
+
merged.update(put_overrides.fault_handler_overrides)
|
|
141
|
+
return merged
|
cfdp/pure/indications.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Pure builders for the CFDP indication effects (design D3/D8).
|
|
2
|
+
|
|
3
|
+
Every pure machine emits the same family of indications — TransactionFinished,
|
|
4
|
+
Suspended, Resumed, Report, Abandoned, plus the various send/receive milestones.
|
|
5
|
+
The tuple *shape* of each indication is protocol-fixed and identical across all
|
|
6
|
+
four machines (only the *source* of the values differs, e.g. Receiver2 reads a
|
|
7
|
+
late-recovered filename from machine state where Receiver1 reads it off the
|
|
8
|
+
frozen transaction). The shapes therefore live here once, as free functions,
|
|
9
|
+
instead of being re-spelled in every machine.
|
|
10
|
+
|
|
11
|
+
These are deliberately **free functions, not methods on a shared base class**.
|
|
12
|
+
They take the values they package explicitly and hold no state, so they cannot
|
|
13
|
+
reach into a machine's ``self`` and cannot reintroduce the kernel / mutable-
|
|
14
|
+
lifecycle coupling the cutover removed with the old ``machines/base.py``
|
|
15
|
+
(issue #22). A machine wraps a call in its own ``self._emit`` so the emission
|
|
16
|
+
point — and which ``self`` fields feed the tuple — stays visible at the call
|
|
17
|
+
site:
|
|
18
|
+
|
|
19
|
+
self._emit(
|
|
20
|
+
indications.transaction_finished(
|
|
21
|
+
self.transaction.id,
|
|
22
|
+
self.condition_code,
|
|
23
|
+
self.file_status,
|
|
24
|
+
self.delivery_code,
|
|
25
|
+
self.filestore_responses,
|
|
26
|
+
self.status_report,
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
Each function returns a single :class:`~cfdp.pure.outputs.EmitIndication`; the
|
|
31
|
+
shell maps ``kind`` to the matching ``CfdpEntity._indication_*`` call (see
|
|
32
|
+
``EffectExecutor._emit_indication``).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from .outputs import EmitIndication, IndicationKind
|
|
38
|
+
from .transaction import TransactionId
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def transaction(tid: TransactionId) -> EmitIndication:
|
|
42
|
+
"""Transaction indication — a transaction has been started (send side)."""
|
|
43
|
+
return EmitIndication(IndicationKind.TRANSACTION, (tid,))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def eof_sent(tid: TransactionId) -> EmitIndication:
|
|
47
|
+
"""EOF-Sent indication — the sender has transmitted its EOF."""
|
|
48
|
+
return EmitIndication(IndicationKind.EOF_SENT, (tid,))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def eof_received(tid: TransactionId) -> EmitIndication:
|
|
52
|
+
"""EOF-Received indication — the receiver has seen the EOF."""
|
|
53
|
+
return EmitIndication(IndicationKind.EOF_RECEIVED, (tid,))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def metadata_received(
|
|
57
|
+
tid: TransactionId,
|
|
58
|
+
source_entity_id: Any,
|
|
59
|
+
file_size: int,
|
|
60
|
+
source_filename: str | None,
|
|
61
|
+
destination_filename: str | None,
|
|
62
|
+
messages_to_user: Any,
|
|
63
|
+
) -> EmitIndication:
|
|
64
|
+
"""Metadata-Received indication — inbound Metadata has been processed.
|
|
65
|
+
|
|
66
|
+
``source_filename`` / ``destination_filename`` / ``messages_to_user`` are
|
|
67
|
+
passed explicitly by the caller: Receiver1 supplies them from the frozen
|
|
68
|
+
transaction (Metadata always arrives first), while Receiver2 supplies them
|
|
69
|
+
from machine state (they may have arrived on a late-recovered Metadata).
|
|
70
|
+
"""
|
|
71
|
+
return EmitIndication(
|
|
72
|
+
IndicationKind.METADATA_RECEIVED,
|
|
73
|
+
(
|
|
74
|
+
tid,
|
|
75
|
+
source_entity_id,
|
|
76
|
+
file_size,
|
|
77
|
+
source_filename,
|
|
78
|
+
destination_filename,
|
|
79
|
+
messages_to_user,
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def filesegment_received(
|
|
85
|
+
tid: TransactionId, offset: int, length: int
|
|
86
|
+
) -> EmitIndication:
|
|
87
|
+
"""File-Segment-Received indication — a Filedata segment has been stored."""
|
|
88
|
+
return EmitIndication(IndicationKind.FILESEGMENT_RECEIVED, (tid, offset, length))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def transaction_finished(
|
|
92
|
+
tid: TransactionId,
|
|
93
|
+
condition_code: Any,
|
|
94
|
+
file_status: Any,
|
|
95
|
+
delivery_code: Any,
|
|
96
|
+
filestore_responses: Any,
|
|
97
|
+
status_report: Any,
|
|
98
|
+
) -> EmitIndication:
|
|
99
|
+
"""Transaction-Finished indication — the transaction has completed."""
|
|
100
|
+
return EmitIndication(
|
|
101
|
+
IndicationKind.TRANSACTION_FINISHED,
|
|
102
|
+
(
|
|
103
|
+
tid,
|
|
104
|
+
condition_code,
|
|
105
|
+
file_status,
|
|
106
|
+
delivery_code,
|
|
107
|
+
filestore_responses,
|
|
108
|
+
status_report,
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def suspended(tid: TransactionId, condition_code: Any) -> EmitIndication:
|
|
114
|
+
"""Suspended indication — the transaction has been suspended."""
|
|
115
|
+
return EmitIndication(IndicationKind.SUSPENDED, (tid, condition_code))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def resumed(tid: TransactionId, progress: int) -> EmitIndication:
|
|
119
|
+
"""Resumed indication — the transaction has been resumed."""
|
|
120
|
+
return EmitIndication(IndicationKind.RESUMED, (tid, progress))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def report(tid: TransactionId, status_report: Any) -> EmitIndication:
|
|
124
|
+
"""Report indication — a requested status report."""
|
|
125
|
+
return EmitIndication(IndicationKind.REPORT, (tid, status_report))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def abandoned(tid: TransactionId, condition_code: Any, progress: int) -> EmitIndication:
|
|
129
|
+
"""Abandoned indication — the transaction has been abandoned."""
|
|
130
|
+
return EmitIndication(IndicationKind.ABANDONED, (tid, condition_code, progress))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Pure state machines (design ``docs/design/machine-decoupling.md``).
|
|
2
|
+
|
|
3
|
+
Each machine here obeys the machine contract exactly:
|
|
4
|
+
|
|
5
|
+
Machine(transaction, config, query_port)
|
|
6
|
+
update_state(event) -> list[Output]
|
|
7
|
+
|
|
8
|
+
No ``kernel`` reference, no threads, no open file descriptors, no wall clock.
|
|
9
|
+
Effects are returned as :mod:`cfdp.pure.outputs` values; local + fast reads go
|
|
10
|
+
through the injected :class:`~cfdp.pure.query_port.QueryPort`; timeouts and
|
|
11
|
+
async results arrive as injected events.
|
|
12
|
+
|
|
13
|
+
These are the only state machines in the codebase. They were originally built in
|
|
14
|
+
parallel with an older, kernel-coupled implementation and verified against it by
|
|
15
|
+
differential trace testing; at the cutover (issue #22) the old implementation was
|
|
16
|
+
removed and its golden traces frozen to ``tests/golden_fixtures/`` (see
|
|
17
|
+
``tests/trace_recorder.py``). The shell that runs them is ``cfdp.shell``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from .receiver1 import Receiver1
|
|
21
|
+
from .receiver2 import Receiver2
|
|
22
|
+
from .sender1 import Sender1
|
|
23
|
+
from .sender2 import Sender2
|
|
24
|
+
|
|
25
|
+
__all__ = ["Sender1", "Receiver1", "Sender2", "Receiver2"]
|