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.
Files changed (53) hide show
  1. cfdp/__init__.py +106 -0
  2. cfdp/checksum.py +94 -0
  3. cfdp/constants.py +153 -0
  4. cfdp/core.py +895 -0
  5. cfdp/event.py +55 -0
  6. cfdp/filestore/__init__.py +2 -0
  7. cfdp/filestore/base.py +99 -0
  8. cfdp/filestore/native.py +162 -0
  9. cfdp/meta/__init__.py +3 -0
  10. cfdp/meta/datafield.py +11 -0
  11. cfdp/meta/filestore.py +127 -0
  12. cfdp/meta/message.py +659 -0
  13. cfdp/pdu/__init__.py +8 -0
  14. cfdp/pdu/ack.py +90 -0
  15. cfdp/pdu/eof.py +123 -0
  16. cfdp/pdu/filedata.py +90 -0
  17. cfdp/pdu/finished.py +99 -0
  18. cfdp/pdu/header.py +159 -0
  19. cfdp/pdu/keep_alive.py +108 -0
  20. cfdp/pdu/metadata.py +211 -0
  21. cfdp/pdu/nak.py +220 -0
  22. cfdp/pdu/prompt.py +75 -0
  23. cfdp/pure/__init__.py +68 -0
  24. cfdp/pure/config.py +141 -0
  25. cfdp/pure/indications.py +130 -0
  26. cfdp/pure/machines/__init__.py +25 -0
  27. cfdp/pure/machines/receiver1.py +433 -0
  28. cfdp/pure/machines/receiver2.py +893 -0
  29. cfdp/pure/machines/sender1.py +400 -0
  30. cfdp/pure/machines/sender2.py +774 -0
  31. cfdp/pure/outputs.py +203 -0
  32. cfdp/pure/query_port.py +48 -0
  33. cfdp/pure/transaction.py +43 -0
  34. cfdp/py.typed +0 -0
  35. cfdp/remote_entity_config.py +12 -0
  36. cfdp/shell/__init__.py +44 -0
  37. cfdp/shell/checksum_service.py +136 -0
  38. cfdp/shell/dispatcher.py +185 -0
  39. cfdp/shell/effect_executor.py +285 -0
  40. cfdp/shell/machine_registry.py +165 -0
  41. cfdp/shell/query_port.py +70 -0
  42. cfdp/shell/timer_service.py +252 -0
  43. cfdp/transaction_handle.py +50 -0
  44. cfdp/transport/__init__.py +0 -0
  45. cfdp/transport/base.py +12 -0
  46. cfdp/transport/spacepacket.py +47 -0
  47. cfdp/transport/udp.py +90 -0
  48. cfdp/transport/zmq.py +54 -0
  49. pycfdp-0.2.2.dist-info/METADATA +76 -0
  50. pycfdp-0.2.2.dist-info/RECORD +53 -0
  51. pycfdp-0.2.2.dist-info/WHEEL +5 -0
  52. pycfdp-0.2.2.dist-info/licenses/LICENSE.txt +19 -0
  53. 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
@@ -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"]