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
@@ -0,0 +1,252 @@
1
+ """The shell's wall-clock timer table (design D6 / issue #17).
2
+
3
+ A pure machine holds no clock and no thread; it only *emits* timer effects
4
+ (``StartTimer/CancelTimer/SuspendTimer/ResumeTimer``) and *receives* an expiry
5
+ as an injected ``EventType`` event. The :class:`TimerService` is the imperative
6
+ shell that owns the actual wall-clock machinery: one timer per
7
+ ``(transaction, kind)``. On expiry it injects the corresponding event onto the
8
+ entity's shared FIFO for the *owning* transaction (design D8 — the same door as
9
+ every other event), so expiry never mutates machine state directly and never
10
+ runs off the event thread.
11
+
12
+ Semantics reproduced exactly from the old ``machines/timer.py`` +
13
+ ``base.Machine`` timer helpers (design D6, Parking-lot item 2):
14
+
15
+ * **start** — (re)start: cancel any live timer for the key, then schedule anew.
16
+ * **cancel** — cancel and *forget*: a cancelled timer cannot be resumed.
17
+ * **suspend** — cancel *without* forgetting: the interval + event are remembered
18
+ so a later :meth:`resume` can restart the same timer (old
19
+ ``suspend_ack_timer`` = ``suspended = True`` + ``cancel()``).
20
+ * **resume** — restart a suspended timer (old ``resume_ack_timer`` =
21
+ ``suspended = False`` + ``restart()``). A no-op if nothing was suspended.
22
+
23
+ The **expiration-limit counter is NOT here** (design D6): it is protocol state
24
+ that lives on the machine and survives suspend. The service just fires the
25
+ event; the machine's handler increments its own counter and decides when the
26
+ limit is reached. Freeze is likewise independent of the timer table — it is a
27
+ machine-state flag, so nothing freeze-related lives here.
28
+
29
+ **Thread-safety.** The default scheduler is :class:`threading.Timer`, so an
30
+ expiry callback runs on a timer thread. The callback only calls
31
+ ``entity.trigger_event`` (a thread-safe queue put), marshalling the expiry onto
32
+ the event thread rather than touching machine state. A lock guards the timer
33
+ table so ``start/cancel/suspend/resume`` (run on the event thread) and an
34
+ expiry callback (run on a timer thread) never corrupt it. :meth:`stop` cancels
35
+ every outstanding timer so no timer thread leaks past entity shutdown.
36
+ """
37
+
38
+ import threading
39
+
40
+ from ..event import Event
41
+
42
+
43
+ class _ThreadingScheduler:
44
+ """The real scheduler: one-shot :class:`threading.Timer` per timer.
45
+
46
+ Injected by default. Tests pass a manual scheduler instead so an expiry can
47
+ be fired synchronously with no wall-clock wait (issue #17 acceptance
48
+ criterion 4). The scheduler seam is intentionally tiny: ``schedule`` returns
49
+ an opaque handle, ``cancel`` cancels it.
50
+ """
51
+
52
+ def schedule(self, interval, callback):
53
+ timer = threading.Timer(interval, callback)
54
+ timer.daemon = True
55
+ timer.start()
56
+ return timer
57
+
58
+ def cancel(self, handle):
59
+ handle.cancel()
60
+ # ``Timer.cancel`` only prevents the action from running; the timer
61
+ # thread then unblocks and exits. Join it (briefly) so a cancelled
62
+ # timer leaves no lingering thread — the suite has active-thread-count
63
+ # assertions around entity shutdown that this must honor. Never join
64
+ # our own thread (an expiry callback that cancels its own timer).
65
+ if handle is not threading.current_thread():
66
+ handle.join(timeout=1)
67
+
68
+
69
+ class _Timer:
70
+ """One entry in the timer table: its schedule state and how to re-arm it.
71
+
72
+ Holds the parameters needed to restart the timer (``interval``, the
73
+ ``event_type`` to inject) so :meth:`resume` can re-arm a suspended timer,
74
+ plus the live scheduler ``handle`` (``None`` when not currently running).
75
+ ``suspended`` records cancel-without-forget so ``resume`` knows to re-arm.
76
+ """
77
+
78
+ __slots__ = ("interval", "event_type", "handle", "suspended")
79
+
80
+ def __init__(self, interval, event_type):
81
+ self.interval = interval
82
+ self.event_type = event_type
83
+ self.handle = None
84
+ self.suspended = False
85
+
86
+
87
+ class TimerService:
88
+ """The ``(tid, kind) -> timer`` table with suspend/resume semantics (D6)."""
89
+
90
+ def __init__(self, entity, scheduler=None):
91
+ self._entity = entity
92
+ self._scheduler = scheduler if scheduler is not None else _ThreadingScheduler()
93
+ # (tid, kind) -> _Timer
94
+ self._timers = {}
95
+ self._lock = threading.Lock()
96
+
97
+ # -- start / cancel ---------------------------------------------------- #
98
+
99
+ def start(self, tid, kind, interval, event_type):
100
+ """(Re)start the ``kind`` timer for ``tid`` (old ``restart``).
101
+
102
+ Cancels any live timer for the key first, then schedules anew. Clears
103
+ any suspended flag: an explicit (re)start supersedes a prior suspend.
104
+ """
105
+ stale_handle = None
106
+ with self._lock:
107
+ existing = self._timers.get((tid, kind))
108
+ if existing is not None:
109
+ # Collect the previous handle for cancellation *outside* the
110
+ # lock (never join under the lock — see :meth:`cancel`).
111
+ stale_handle = existing.handle
112
+ timer = _Timer(interval, event_type)
113
+ self._timers[(tid, kind)] = timer
114
+ self._arm(tid, kind, timer)
115
+ if stale_handle is not None:
116
+ self._scheduler.cancel(stale_handle)
117
+
118
+ def cancel(self, tid, kind):
119
+ """Cancel and *forget* the ``kind`` timer for ``tid`` (old ``cancel``).
120
+
121
+ A cancelled timer is removed entirely, so a later :meth:`resume` brings
122
+ nothing back. This is the difference from :meth:`suspend`.
123
+
124
+ The table entry is popped *inside* the lock so a concurrently-firing
125
+ :meth:`_on_expiry` sees ``timer is None`` and no-ops (no stale event is
126
+ injected). The scheduler ``cancel`` — which may ``join`` the timer
127
+ thread — runs *outside* the lock, so the event thread never blocks
128
+ joining a timer thread that is itself waiting on this lock.
129
+ """
130
+ handle = None
131
+ with self._lock:
132
+ timer = self._timers.pop((tid, kind), None)
133
+ if timer is not None:
134
+ handle = timer.handle
135
+ if handle is not None:
136
+ self._scheduler.cancel(handle)
137
+
138
+ # -- suspend / resume (cancel-without-forget / restart) ---------------- #
139
+
140
+ def suspend(self, tid, kind):
141
+ """Suspend the ``kind`` timer for ``tid`` (cancel-without-forget).
142
+
143
+ Mirrors the old ``suspend_ack_timer``: cancel the running timer but keep
144
+ the entry (and its interval/event) so :meth:`resume` can restart it.
145
+ A no-op if there is no such timer.
146
+ """
147
+ handle = None
148
+ with self._lock:
149
+ timer = self._timers.get((tid, kind))
150
+ if timer is None:
151
+ return
152
+ handle = timer.handle
153
+ timer.handle = None
154
+ timer.suspended = True
155
+ if handle is not None:
156
+ self._scheduler.cancel(handle)
157
+
158
+ def resume(self, tid, kind):
159
+ """Resume a suspended ``kind`` timer for ``tid`` (old ``resume`` = restart).
160
+
161
+ Only re-arms a timer that was actually suspended; resuming a running or
162
+ unknown timer is a no-op (it must not double-schedule).
163
+ """
164
+ with self._lock:
165
+ timer = self._timers.get((tid, kind))
166
+ if timer is None or not timer.suspended:
167
+ return
168
+ timer.suspended = False
169
+ self._arm(tid, kind, timer)
170
+
171
+ # -- cleanup ----------------------------------------------------------- #
172
+
173
+ def cancel_all(self, tid):
174
+ """Cancel and forget every timer for one transaction (completion/reap)."""
175
+ handles = []
176
+ with self._lock:
177
+ for t, kind in list(self._timers.keys()):
178
+ if t != tid:
179
+ continue
180
+ timer = self._timers.pop((t, kind))
181
+ if timer.handle is not None:
182
+ handles.append(timer.handle)
183
+ for handle in handles:
184
+ self._scheduler.cancel(handle)
185
+
186
+ def stop(self):
187
+ """Cancel every outstanding timer (entity shutdown). No thread leaks."""
188
+ handles = []
189
+ with self._lock:
190
+ for timer in self._timers.values():
191
+ if timer.handle is not None:
192
+ handles.append(timer.handle)
193
+ self._timers.clear()
194
+ for handle in handles:
195
+ self._scheduler.cancel(handle)
196
+
197
+ # -- internals --------------------------------------------------------- #
198
+
199
+ def _arm(self, tid, kind, timer):
200
+ """Schedule ``timer`` so it injects its event on expiry. Caller holds lock."""
201
+ timer.handle = self._scheduler.schedule(
202
+ timer.interval, self._make_callback(tid, kind)
203
+ )
204
+
205
+ def _make_callback(self, tid, kind):
206
+ def _expired():
207
+ self._on_expiry(tid, kind)
208
+
209
+ return _expired
210
+
211
+ def _on_expiry(self, tid, kind):
212
+ """Timer-thread callback: marshal the expiry onto the entity FIFO (D8).
213
+
214
+ Reads the event type under the lock (the timer may have been cancelled
215
+ or replaced in a race), then injects the expiry outside the lock via the
216
+ entity's thread-safe ``trigger_event``.
217
+
218
+ **No machine mutation here (design D6, issue #29).** The ACK/NAK
219
+ expiration-limit counters are protocol state on the machine. Bumping them
220
+ from this timer thread was a lost-update race with the event thread (an
221
+ expiry callback reaching into ``entity.machines[tid]`` and calling
222
+ ``note_ack_timeout()``/``note_nak_timeout()`` while the event thread also
223
+ mutates the same machine). The bump now lives in
224
+ :meth:`cfdp.shell.dispatcher.Dispatcher.dispatch`, which runs
225
+ single-threaded on the event thread and bumps the counter immediately
226
+ before ``update_state`` reads it. E25/E26 are produced ONLY by this
227
+ service, so the dispatcher is the one safe place to do it. This callback
228
+ therefore only marshals the expiry event onto the FIFO and never touches
229
+ any machine state.
230
+ """
231
+ with self._lock:
232
+ timer = self._timers.get((tid, kind))
233
+ if timer is None:
234
+ return
235
+ event_type = timer.event_type
236
+ transaction = _TimerTransaction(tid)
237
+ self._entity.trigger_event(Event(transaction, event_type))
238
+
239
+
240
+ class _TimerTransaction:
241
+ """A minimal transaction identity carrier for an injected expiry event.
242
+
243
+ The dispatcher routes events by ``event.transaction.id``, so the injected
244
+ expiry only needs to carry the owning ``tid``. This avoids the timer holding
245
+ a back-reference to the (frozen) transaction or the machine — the service
246
+ stays decoupled from both (design D6).
247
+ """
248
+
249
+ __slots__ = ("id",)
250
+
251
+ def __init__(self, tid):
252
+ self.id = tid
@@ -0,0 +1,50 @@
1
+ import weakref
2
+
3
+
4
+ class TransactionHandle:
5
+ """Rich return value from CfdpEntity.put() providing per-transaction controls."""
6
+
7
+ def __init__(self, entity, transaction_id):
8
+ self._entity_ref = weakref.ref(entity)
9
+ self._id = transaction_id
10
+
11
+ @property
12
+ def id(self):
13
+ return self._id
14
+
15
+ def _entity(self):
16
+ entity = self._entity_ref()
17
+ if entity is None or not entity._started:
18
+ raise RuntimeError("CfdpEntity has been stopped")
19
+ return entity
20
+
21
+ def cancel(self):
22
+ self._entity().cancel(self._id)
23
+
24
+ def suspend(self):
25
+ self._entity().suspend(self._id)
26
+
27
+ def resume(self):
28
+ self._entity().resume(self._id)
29
+
30
+ def report(self):
31
+ self._entity().report(self._id)
32
+
33
+ def is_complete(self):
34
+ entity = self._entity_ref()
35
+ if entity is None:
36
+ return True
37
+ return entity.is_complete(self._id)
38
+
39
+ def __iter__(self):
40
+ return iter(self._id)
41
+
42
+ def __eq__(self, other):
43
+ if isinstance(other, TransactionHandle):
44
+ return self._id == other._id
45
+ if isinstance(other, tuple):
46
+ return self._id == other
47
+ return NotImplemented
48
+
49
+ def __hash__(self):
50
+ return hash(self._id)
File without changes
cfdp/transport/base.py ADDED
@@ -0,0 +1,12 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class Transport(ABC):
5
+ @abstractmethod
6
+ def send(self, data: bytes) -> None: ...
7
+ @abstractmethod
8
+ def start(self, on_receive) -> None: ...
9
+ @abstractmethod
10
+ def stop(self) -> None: ...
11
+ def is_ready(self) -> bool:
12
+ return True
@@ -0,0 +1,47 @@
1
+ from spacepacket import SequenceFlags, SpacePacket
2
+
3
+ from .base import Transport
4
+
5
+
6
+ class SpacePacketTransport(Transport):
7
+ """Space Packet Protocol Transport for CFDP PDUs.
8
+
9
+ Wraps another Transport that carries raw bytes (e.g. UdpTransport).
10
+ CfdpEntity calls start(on_receive); this in turn calls the inner
11
+ transport's start() so no separate bind/start step is needed for it.
12
+ """
13
+
14
+ def __init__(self, apid, transport, packet_type):
15
+ super().__init__()
16
+ self.apid = apid
17
+ self.packet_type = packet_type
18
+ self._inner = transport
19
+ self._packet_sequence_count = 0
20
+ self._on_receive = None
21
+
22
+ def _get_next_packet_sequence_count(self):
23
+ self._packet_sequence_count += 1
24
+ return self._packet_sequence_count
25
+
26
+ def send(self, data: bytes) -> None:
27
+ space_packet = SpacePacket(
28
+ packet_type=self.packet_type,
29
+ packet_sec_hdr_flag=False,
30
+ apid=self.apid,
31
+ sequence_flags=SequenceFlags.UNSEGMENTED,
32
+ packet_sequence_count=self._get_next_packet_sequence_count(),
33
+ packet_data_field=data,
34
+ )
35
+ self._inner.send(space_packet.encode())
36
+
37
+ def start(self, on_receive) -> None:
38
+ self._on_receive = on_receive
39
+ self._inner.start(self._incoming_pdu_handler)
40
+
41
+ def stop(self) -> None:
42
+ self._inner.stop()
43
+
44
+ def _incoming_pdu_handler(self, raw: bytes) -> None:
45
+ packet = SpacePacket.decode(raw)
46
+ if self._on_receive:
47
+ self._on_receive(packet.packet_data_field)
cfdp/transport/udp.py ADDED
@@ -0,0 +1,90 @@
1
+ import socket
2
+ import threading
3
+
4
+ from .base import Transport
5
+
6
+ DEFAULT_MAXIMUM_PACKET_LENGTH = 4096
7
+
8
+
9
+ class UdpTransport(Transport):
10
+ """UDP Transport for CFDP PDUs.
11
+
12
+ This transport is mostly used for testing and for prototypes.
13
+ The transport is realized by making each entity a host, which does `bind` to a port.
14
+ The other entity then sends directly PDUs to this other entity. Since UDP is connection-less
15
+ there is no need to establish a connection beforehand.
16
+
17
+ Call `bind(host, port)` to set the local address, then pass to CfdpEntity which calls
18
+ `start(on_receive)` to begin receiving. Call `stop()` to shut down.
19
+
20
+ Routing dictionary
21
+ ------------------
22
+ The ``routing`` parameter maps destination keys to lists of ``(host, port)`` tuples.
23
+
24
+ The special key ``"*"`` causes every outgoing PDU to be sent to all listed addresses::
25
+
26
+ routing = {"*": [("127.0.0.1", 5222)]}
27
+
28
+ Tuple keys ``(host, port)`` forward incoming PDUs received *from* that address on to
29
+ one or more other destinations (relay / repeater mode)::
30
+
31
+ routing = {
32
+ ("127.0.0.1", 5111): [("127.0.0.1", 5333)],
33
+ }
34
+
35
+ A typical two-entity local setup (entity 1 sends to entity 2 on port 5222)::
36
+
37
+ transport = UdpTransport(routing={"*": [("127.0.0.1", 5222)]})
38
+ transport.bind("127.0.0.1", 5111)
39
+ """
40
+
41
+ def __init__(self, routing, maximum_packet_length=DEFAULT_MAXIMUM_PACKET_LENGTH):
42
+ super().__init__()
43
+ self.routing = routing
44
+ self.maximum_packet_length = maximum_packet_length
45
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
46
+ self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4194304)
47
+ self._thread = threading.Thread(target=self._incoming_pdu_handler, daemon=True)
48
+ self._thread.kill = False
49
+ self._on_receive = None
50
+ self._addr = None
51
+ self.send_count = 0
52
+ self.recv_count = 0
53
+
54
+ def bind(self, host, port):
55
+ self._addr = (host, port)
56
+ self._socket.bind((host, port))
57
+
58
+ def send(self, data: bytes) -> None:
59
+ self.send_count += 1
60
+ if self.routing and "*" in self.routing:
61
+ for address in self.routing["*"]:
62
+ host, port = address
63
+ self._socket.sendto(data, (host, int(port)))
64
+
65
+ def start(self, on_receive) -> None:
66
+ self._on_receive = on_receive
67
+ self._thread.kill = False
68
+ self._thread.start()
69
+
70
+ def stop(self) -> None:
71
+ self._thread.kill = True
72
+ if self._addr:
73
+ self._socket.sendto(b"0", self._addr) # break recvfrom loop
74
+ self._thread.join()
75
+ self._socket.close()
76
+
77
+ def _incoming_pdu_handler(self):
78
+ thread = threading.current_thread()
79
+ buffer_size = 10 * self.maximum_packet_length
80
+
81
+ while not thread.kill:
82
+ pdu, addr = self._socket.recvfrom(buffer_size)
83
+ self.recv_count += 1
84
+ if thread.kill:
85
+ break
86
+ if self.routing and addr in self.routing:
87
+ for dest in self.routing[addr]:
88
+ self._socket.sendto(pdu, dest)
89
+ if self._on_receive:
90
+ self._on_receive(pdu)
cfdp/transport/zmq.py ADDED
@@ -0,0 +1,54 @@
1
+ import threading
2
+
3
+ import zmq
4
+
5
+ from .base import Transport
6
+
7
+
8
+ class ZmqTransport(Transport):
9
+ """ZMQ Transport for CFDP PDUs.
10
+
11
+ This transport is mostly used for testing and for prototypes.
12
+ A connection is established by making one entity the host, which does `bind` to a port,
13
+ and the other entity the client, which does `connect` to the host.
14
+
15
+ Call `bind(host, port)` or `connect(host, port)` to set up the socket, then pass to
16
+ CfdpEntity which calls `start(on_receive)` to begin receiving. Call `stop()` to shut down.
17
+ """
18
+
19
+ def __init__(self):
20
+ super().__init__()
21
+ self.context = zmq.Context()
22
+ self.socket = self.context.socket(zmq.PAIR)
23
+ self._thread = threading.Thread(target=self._incoming_pdu_handler, daemon=True)
24
+ self._thread.kill = False
25
+ self._on_receive = None
26
+
27
+ def bind(self, host, port):
28
+ self.socket.bind(f"tcp://{host}:{port}")
29
+
30
+ def connect(self, host, port):
31
+ self.socket.connect(f"tcp://{host}:{port}")
32
+
33
+ def send(self, data: bytes) -> None:
34
+ self.socket.send(data)
35
+
36
+ def start(self, on_receive) -> None:
37
+ self._on_receive = on_receive
38
+ self._thread.kill = False
39
+ self._thread.start()
40
+
41
+ def stop(self) -> None:
42
+ self._thread.kill = True
43
+ self._thread.join()
44
+ self.context.destroy()
45
+
46
+ def _incoming_pdu_handler(self):
47
+ thread = threading.current_thread()
48
+ while not thread.kill:
49
+ if self.socket.poll(100):
50
+ if thread.kill:
51
+ break
52
+ pdu = self.socket.recv()
53
+ if self._on_receive:
54
+ self._on_receive(pdu)
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: pycfdp
3
+ Version: 0.2.2
4
+ Summary: Python implementation of CCSDS File Delivery Protocol (Modernized Fork)
5
+ Author-email: Alex Pope <alexpopester@gmail.com>, "LibreCube (Original Codebase)" <info@librecube.org>
6
+ License: MIT License
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE.txt
10
+ Provides-Extra: zmq
11
+ Requires-Dist: pyzmq; extra == "zmq"
12
+ Provides-Extra: spacepacket
13
+ Requires-Dist: spacepacket; extra == "spacepacket"
14
+ Provides-Extra: all
15
+ Requires-Dist: pyzmq; extra == "all"
16
+ Requires-Dist: spacepacket; extra == "all"
17
+ Dynamic: license-file
18
+
19
+ # Python CFDP, Version 0.2
20
+
21
+ The CCSDS File Delivery Protocol (CFDP) was developed by the
22
+ [Consultative Committee for Space Data Systems (CCSDS)](https://public.ccsds.org).
23
+ The CFDP protocol provides reliable transfer of files from one endpoint to
24
+ another and has been designed to work well over space links that suffer
25
+ from outtakes and long delays. The basic operation of CFDP is to transfer a
26
+ file from a sender to a receiver (both referred to as CFDP entities). The
27
+ sender and receiver must be configured and running at the same time to perform
28
+ a file transfer. It can be used to perform space to ground, ground to space,
29
+ space to space, and ground to ground file transfers. For example it can be used
30
+ to transfer science data from satellite over mission control to the science
31
+ center in an automated fashion with minimal to no human intervention.
32
+
33
+ This Python module is an implementation of the CCSDS File Delivery Protocol.
34
+ It supports all features as outlined in the latest version of the [CFDP Blue Book](docs/727x0b5.pdf). These are:
35
+
36
+ - Class 1 (unacknowledged) file transfer
37
+ - Class 2 (acknowledged) file transfer
38
+ - Filestore requests
39
+ - Proxy operations
40
+ - Directory listing request
41
+ - Native filestore implementation
42
+ - UDP as default transport layer (optional ZMQ)
43
+
44
+ ## Installation
45
+
46
+ Clone the repository and then install via pip:
47
+
48
+ ```
49
+ pip install cfdp
50
+ ```
51
+
52
+ > Depending on the transport layer to use with CFDP, one needs to install additional dependencies.
53
+
54
+ ## Documentation
55
+
56
+ Find the detailed documentation [here](docs/README.md).
57
+
58
+ ## Tests
59
+
60
+ The protocol is tested for cross-support as outlined in CCSDS CFDP Yellow Book.
61
+ See [tests/README.md](tests/README.md) for details.
62
+
63
+ ## Contribute
64
+
65
+ To learn more on how to successfully contribute please read the contributing
66
+ information in the [LibreCube documentation](https://librecube.gitlab.io/).
67
+
68
+ ## Support
69
+
70
+ If you are having issues, please let us know. Reach us at
71
+ [Matrix](https://app.element.io/#/room/#librecube.org:matrix.org)
72
+ or via [Email](mailto:info@librecube.org).
73
+
74
+ ## License
75
+
76
+ The project is licensed under the MIT license. See the [LICENSE](./LICENSE.txt) file for details.
@@ -0,0 +1,53 @@
1
+ cfdp/__init__.py,sha256=LL8SZEEFxM8VuTjiyVAwmLLv87F_kISxR5B_o5SHcWc,2544
2
+ cfdp/checksum.py,sha256=bdxNA5szavRewdMXV75UApySmkPHNPHieYcqqE8skfI,2822
3
+ cfdp/constants.py,sha256=eZi0zmPuPVbs_G-_DnP4lwCUXZXksjC3sfr-iC3WtMU,3728
4
+ cfdp/core.py,sha256=KiQzyXI0OOmfofkmsXAX1AkkhsIw5XV5Q_oNeNrgV6U,37885
5
+ cfdp/event.py,sha256=TnG1pS9Jn-MFTCJO4rw1Nzko1ES8bBYN--LoENsWw3g,2651
6
+ cfdp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ cfdp/remote_entity_config.py,sha256=Z6QRBZM2gGgXy1LKiMO8ULc2ElbmIwK68YkjD9psmkw,403
8
+ cfdp/transaction_handle.py,sha256=oP8EXwQgKQaKL00YNhfciPXtga2YD0ap_hhkylczflc,1256
9
+ cfdp/filestore/__init__.py,sha256=ikbR4utD7WuqxPp3FnXDyhceAxMtzTUwheKDCfvmP0c,83
10
+ cfdp/filestore/base.py,sha256=_lZiY1f-cwpkKEcCRCV7wVysAllSjEr4nXfCSfZlEf0,2418
11
+ cfdp/filestore/native.py,sha256=GSPAML-u9VT5Ijrak9G9nExSe627aFQ401OXkf-WuAo,5286
12
+ cfdp/meta/__init__.py,sha256=B4cG2t1B_9imk4qGSDghBCGIycC6v9xfCvYnIAS_F5c,73
13
+ cfdp/meta/datafield.py,sha256=D_gcefotAxyegNOtlASUM0DEPsa6p6g6Wqf5OH5F-zI,279
14
+ cfdp/meta/filestore.py,sha256=tS2Kz2QMhHg54f3BaNcIHTE778m56QJZiCuo1Pvk__A,5048
15
+ cfdp/meta/message.py,sha256=DKy6rSi8qkyELCjza50Y7BfCu0YiQSpEqujZD2lDLZw,22459
16
+ cfdp/pdu/__init__.py,sha256=ht0APp7MeMvOiBvANzuF6x4M91NtdOPSH2Ega3cGFs0,177
17
+ cfdp/pdu/ack.py,sha256=0OWzo4AwwbEVnu4p4A8kdwrN_9v7mcnZ3k-UwFRem78,3358
18
+ cfdp/pdu/eof.py,sha256=jNC6DxRu-IPDYzkksNniR9RFdZqv_-sLeYZ5Jyjqdmo,4710
19
+ cfdp/pdu/filedata.py,sha256=-Rk6MgwCypsE74fTZQoJA80dNuktLJHuny_Q9WmUEV0,3307
20
+ cfdp/pdu/finished.py,sha256=NuuLI24FuXao21AqgfEiQKrAAtXVfPn3anvKaCbfDEk,3603
21
+ cfdp/pdu/header.py,sha256=l-S2IaIrp8lNcz4bx9iIqmPwxXtJ5sCsm1C9gWeK5FM,5564
22
+ cfdp/pdu/keep_alive.py,sha256=Pc9_hbHe-6rzkhll6fgj-dUuUwysGrVK8nmqZrbp-Zc,3802
23
+ cfdp/pdu/metadata.py,sha256=6qekV8RnT-AxBeQN75HBBA5lvyiyU6nkT9TEbiQlW7c,7211
24
+ cfdp/pdu/nak.py,sha256=6noKpP-zBg0Nuwasnq6vTOKCAJoVZL3zhtEqLXTSr-E,8250
25
+ cfdp/pdu/prompt.py,sha256=d0Zn-2JSl-MA-vRj7mZmeIMOaKCCvVG5YSo1rz8dD3M,2779
26
+ cfdp/pure/__init__.py,sha256=uHjS3O71KTbh1pO4MtuW0sBPvC01ECVXWpOOtTROvSQ,1669
27
+ cfdp/pure/config.py,sha256=X4bWoyZFxJ-4k36MZMae0GygOie1DCI0gbbZyQyPsjU,5451
28
+ cfdp/pure/indications.py,sha256=CMqcm0Ewm9hvb6U4gjX5AZ6uKFQ8cSRMkul99zQ8VWE,4761
29
+ cfdp/pure/outputs.py,sha256=l42GkibAz7xfr3kC2rSZOrgk0dfaoY2X0gOaTUKrLTU,6151
30
+ cfdp/pure/query_port.py,sha256=W6E91sDgXbqvAwBcPrcPfhNRvkbkErD5phKeq56KvCc,1930
31
+ cfdp/pure/transaction.py,sha256=JJUQo6Anl_LG0-Ku6e1BY8MYgU4r-In52zZanoOCqqY,1792
32
+ cfdp/pure/machines/__init__.py,sha256=Kq3mhWjl-4vZMqIVYz6NoonWVZP69aizo6e7HVWzcQc,1067
33
+ cfdp/pure/machines/receiver1.py,sha256=JhGKlIb93Ea63IKhi-5Hy9PGh2CXF1LUV1hWKtcr_FA,17250
34
+ cfdp/pure/machines/receiver2.py,sha256=775L-ykqIFkyGF3a5O_48fAfD7eZNbs5eQwYddyYx5M,34892
35
+ cfdp/pure/machines/sender1.py,sha256=I99F3RO_xRhYzYYxUQkVzVxbySwhrMncWkimBOsYcF0,15972
36
+ cfdp/pure/machines/sender2.py,sha256=ewQggAzj9R0of4KvJEZAzWwzptdvtPQJWoH2xb6oYmA,30902
37
+ cfdp/shell/__init__.py,sha256=XF-aVp8CB5nK-wU4uMHqtn1yz86fwLQ8TZOXuQxGb3c,2129
38
+ cfdp/shell/checksum_service.py,sha256=qxQn5eqbdtz_Dxn_gsEJNRiTBunTvveDHWDQ5GmUZZE,6201
39
+ cfdp/shell/dispatcher.py,sha256=lvUck7rt33Th14xj05HksHX1QkvibOHdZ9oTE5MpqOs,8773
40
+ cfdp/shell/effect_executor.py,sha256=atDkNmTHrh__2Mt-dgn1IdJJtox2jmjDXHBjK5wPl5Y,12369
41
+ cfdp/shell/machine_registry.py,sha256=qcUzAB8yL4n1ss8C8MMm3vSuzA2wzNxfI9yN41cn_EI,6649
42
+ cfdp/shell/query_port.py,sha256=Yy18A7nM8p9XjMleo02yJwlc69hKTzd_j3lvwbhIB3M,2809
43
+ cfdp/shell/timer_service.py,sha256=We_xX1fw3DCa4zQAdClP_2DJHPQSpmijorYiJSlXxxE,10767
44
+ cfdp/transport/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ cfdp/transport/base.py,sha256=EknCGBCTSTan69Z6mStyfHt1Ss4465_bV8XcwbeNagc,294
46
+ cfdp/transport/spacepacket.py,sha256=yPBO5ffhRpowA9tqIGqIQN_Ukwb4VgWx7jxC-nZyWMs,1562
47
+ cfdp/transport/udp.py,sha256=-bAsS-uf800eyT_nVCalik8ODQ3_6C7-eHZeenGOEqA,3213
48
+ cfdp/transport/zmq.py,sha256=y17olLqBqgd8_6lofdrztUh3M1phRBm6WCaJp7pGiXY,1680
49
+ pycfdp-0.2.2.dist-info/licenses/LICENSE.txt,sha256=UIp30ue1HZit7tMmSK0SS3swJBqOcLLnLJn5LY5YdNE,1036
50
+ pycfdp-0.2.2.dist-info/METADATA,sha256=i_8EQ3xDEGfzrnvrOdpveToquHERG5moj6iIptGpPVU,2747
51
+ pycfdp-0.2.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
52
+ pycfdp-0.2.2.dist-info/top_level.txt,sha256=Z1jvG2hdlWsdnR6V1AAoRebRKU-TWj41bUrGXPciBuk,5
53
+ pycfdp-0.2.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,19 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ cfdp