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,285 @@
1
+ """The effect executor and its file table (design D7/D10).
2
+
3
+ A pure machine never touches the world; it returns effects. The
4
+ :class:`EffectExecutor` interprets those effects against the real filestore,
5
+ transport, and indication/message plumbing, and owns the one place where open
6
+ OS file handles live: the ``tid -> handle`` :class:`FileTable` (design D10).
7
+
8
+ The executor is deliberately *thin logic, real I/O*: each ``execute`` call maps
9
+ exactly one :class:`~cfdp.pure.outputs.Output` effect to its side effect. The
10
+ :class:`~cfdp.shell.dispatcher.Dispatcher` owns the exception boundary around
11
+ these calls (design D4/D7), so the executor itself just does the work and lets
12
+ exceptions propagate.
13
+ """
14
+
15
+ from .. import logger
16
+ from ..event import EventType
17
+ from ..filestore.base import FileHandle
18
+ from ..meta import execute_filestore_requests
19
+ from ..pure.outputs import (
20
+ CancelTimer,
21
+ CloseFile,
22
+ EmitIndication,
23
+ ExecuteFilestoreRequests,
24
+ Finalize,
25
+ InternalEvent,
26
+ OpenSource,
27
+ OpenTemp,
28
+ Output,
29
+ PostUserMessage,
30
+ RequestChecksum,
31
+ ResumeTimer,
32
+ ScheduleEvent,
33
+ SendPdu,
34
+ StartTimer,
35
+ SuspendTimer,
36
+ TimerKind,
37
+ WriteSegment,
38
+ )
39
+ from ..pure.transaction import Transaction, TransactionId
40
+
41
+ # Which EventType a given timer kind injects on expiry (design D6). The pure
42
+ # machine emits ``StartTimer(kind, interval)`` without naming the event; the
43
+ # shell owns this fixed kind -> event mapping so the machine stays agnostic of
44
+ # how expiry re-enters the FIFO.
45
+ _TIMER_EXPIRY_EVENT = {
46
+ TimerKind.INACTIVITY: EventType.E27_INACTIVITY_TIMEOUT,
47
+ TimerKind.ACK: EventType.E25_ACK_TIMEOUT,
48
+ TimerKind.NAK: EventType.E26_NAK_TIMEOUT,
49
+ }
50
+
51
+
52
+ class FileTable:
53
+ """The ``tid -> open handle`` map (design D10).
54
+
55
+ A machine that never opened a file has no entry, so cleanup is a no-op for
56
+ it — this kills the class of ``AttributeError`` the old ``shutdown()`` could
57
+ raise (design D10 / issue 01).
58
+
59
+ Thread-safety invariant (issue #21): ``get(tid)`` is read by the async
60
+ checksum worker thread while the event thread calls ``set``/``pop``/
61
+ ``cleanup``. This is lock-free-safe because the receive-side lifecycle
62
+ guarantees the worker's read never overlaps a close: the handle stays in the
63
+ table until the transaction is reaped, and reaping happens only after the
64
+ checksum result (E42) has been processed, i.e. after the worker has finished.
65
+ """
66
+
67
+ def __init__(self) -> None:
68
+ self._handles: dict[TransactionId, FileHandle] = {}
69
+
70
+ def get(self, tid: TransactionId) -> FileHandle | None:
71
+ return self._handles.get(tid)
72
+
73
+ def set(self, tid: TransactionId, handle) -> None:
74
+ self._handles[tid] = handle
75
+
76
+ def pop(self, tid: TransactionId):
77
+ return self._handles.pop(tid, None)
78
+
79
+ def __contains__(self, tid: TransactionId) -> bool:
80
+ return tid in self._handles
81
+
82
+ def close_all(self) -> None:
83
+ """Close and forget every open handle (entity shutdown / reaping)."""
84
+ for tid, handle in list(self._handles.items()):
85
+ try:
86
+ if handle is not None:
87
+ handle.close()
88
+ except Exception:
89
+ logger.exception("Error closing file handle for %s", tid)
90
+ self._handles.pop(tid, None)
91
+
92
+ def cleanup(self, tid: TransactionId) -> None:
93
+ """Close and forget the handle for one transaction, if any."""
94
+ handle = self._handles.pop(tid, None)
95
+ if handle is not None:
96
+ try:
97
+ handle.close()
98
+ except Exception:
99
+ logger.exception("Error closing file handle for %s", tid)
100
+
101
+
102
+ class EffectExecutor:
103
+ """Interprets effect outputs against real I/O (design D7).
104
+
105
+ Owns the :class:`FileTable`; opens/writes/finalizes/closes files, encodes and
106
+ sends PDUs, and fans indications and user messages out through the entity's
107
+ existing indication methods and message queue.
108
+ """
109
+
110
+ def __init__(
111
+ self, entity, file_table=None, timer_service=None, checksum_service=None
112
+ ):
113
+ self.entity = entity
114
+ self.files = file_table if file_table is not None else FileTable()
115
+ # The wall-clock timer table (design D6 / issue #17). Owns real timers
116
+ # per (tid, kind) and injects the expiry event onto the entity FIFO.
117
+ self.timers = timer_service
118
+ # The async-checksum worker (design R1 / issue #21). Computes a whole-file
119
+ # checksum off the event thread and injects E42_CHECKSUM_READY. Optional
120
+ # so the Class-1 shell-seam unit tests (which never request a checksum via
121
+ # the executor) can construct an executor without one.
122
+ self.checksums = checksum_service
123
+ # Direct-to-destination bookkeeping (write_to_destination_directly).
124
+ # ``_direct_dest`` maps a tid to the destination filename its handle was
125
+ # opened on (so a failed transfer's partial file can be deleted);
126
+ # ``_direct_committed`` marks tids whose data was verified/Finalized, so
127
+ # the following CloseFile keeps the file instead of deleting it.
128
+ self._direct_dest: dict[TransactionId, str] = {}
129
+ self._direct_committed: set[TransactionId] = set()
130
+
131
+ # -- the single execute door ------------------------------------------- #
132
+
133
+ def execute(
134
+ self, tid: TransactionId, transaction: Transaction, effect: Output
135
+ ) -> None:
136
+ """Execute one effect for the machine owning ``tid``.
137
+
138
+ Timer effects are handled by the dispatcher/timer service, not here;
139
+ internal events are re-enqueued by the dispatcher. This method covers
140
+ only the world-touching effects the Class-1 machines emit.
141
+ """
142
+ if isinstance(effect, SendPdu):
143
+ self.entity.transport.send(effect.pdu.encode())
144
+
145
+ elif isinstance(effect, OpenSource):
146
+ self.files.set(tid, self.entity.filestore.open(transaction.source_filename))
147
+
148
+ elif isinstance(effect, OpenTemp):
149
+ dest_name = transaction.destination_filename
150
+ if self.entity.write_to_destination_directly and dest_name:
151
+ # Direct-to-destination mode: write incoming data straight into
152
+ # the target file instead of a temp file, so a remote-service
153
+ # filestore can show download progress. Open read/write (``w+b``)
154
+ # so the async checksum can read the file back before Finished.
155
+ # If the destination is not yet known (Class-2 recovery with a
156
+ # lost Metadata, where only the machine holds the resolved name),
157
+ # fall back to a temp file and the normal Finalize copy.
158
+ self.files.set(tid, self.entity.filestore.open(dest_name, "w+b"))
159
+ self._direct_dest[tid] = dest_name
160
+ else:
161
+ self.files.set(tid, self.entity.filestore.open_tempfile())
162
+
163
+ elif isinstance(effect, WriteSegment):
164
+ handle = self.files.get(tid)
165
+ handle.seek(effect.offset)
166
+ handle.write(effect.data)
167
+
168
+ elif isinstance(effect, ExecuteFilestoreRequests):
169
+ if effect.requests:
170
+ execute_filestore_requests(self.entity, effect.requests)
171
+
172
+ elif isinstance(effect, Finalize):
173
+ self._finalize(tid, transaction, effect.destination_filename)
174
+
175
+ elif isinstance(effect, CloseFile):
176
+ handle = self.files.get(tid)
177
+ if handle is not None:
178
+ handle.close()
179
+ if tid in self._direct_dest:
180
+ # Direct-to-destination mode: the target file was written in
181
+ # place. If the transfer never reached Finalize (checksum/size
182
+ # fault, cancel, abandon), delete the partial file so a failed
183
+ # transfer never leaves a corrupt file behind. Pop the tracking
184
+ # so the second CloseFile on the happy path is a plain no-op.
185
+ dest_name = self._direct_dest.pop(tid)
186
+ if tid not in self._direct_committed:
187
+ try:
188
+ self.entity.filestore.delete_file(dest_name)
189
+ except Exception:
190
+ logger.exception(
191
+ "Error deleting partial direct-write file for %s", tid
192
+ )
193
+ self._direct_committed.discard(tid)
194
+ # Mirror the old machine: close_tempfile does NOT drop the handle,
195
+ # so a double CloseFile on the happy EOF path is a no-op the second
196
+ # time (the file object tolerates a repeat close). The handle is
197
+ # only dropped from the table when the transaction is reaped, so the
198
+ # QueryPort can still read the temp file between close and reap.
199
+
200
+ elif isinstance(effect, EmitIndication):
201
+ self._emit_indication(effect)
202
+
203
+ elif isinstance(effect, PostUserMessage):
204
+ self.entity._messages_to_user_queue.put(effect.message)
205
+
206
+ elif isinstance(effect, StartTimer):
207
+ self.timers.start(
208
+ tid, effect.kind, effect.interval, _TIMER_EXPIRY_EVENT[effect.kind]
209
+ )
210
+
211
+ elif isinstance(effect, CancelTimer):
212
+ self.timers.cancel(tid, effect.kind)
213
+
214
+ elif isinstance(effect, SuspendTimer):
215
+ self.timers.suspend(tid, effect.kind)
216
+
217
+ elif isinstance(effect, ResumeTimer):
218
+ self.timers.resume(tid, effect.kind)
219
+
220
+ elif isinstance(effect, RequestChecksum):
221
+ # Compute the whole-file checksum off the event thread (issue #21);
222
+ # the result returns as an injected E42_CHECKSUM_READY event.
223
+ self.checksums.request(
224
+ tid, transaction, effect.checksum_type, effect.from_temp_handle
225
+ )
226
+
227
+ elif isinstance(effect, (InternalEvent, ScheduleEvent)):
228
+ # Should be routed by the dispatcher, never reach the executor.
229
+ raise TypeError("internal events are not executor effects")
230
+
231
+ else:
232
+ raise TypeError(f"unknown effect: {effect!r}")
233
+
234
+ # -- file lifecycle ---------------------------------------------------- #
235
+
236
+ def _finalize(
237
+ self,
238
+ tid: TransactionId,
239
+ transaction: Transaction,
240
+ destination_filename=None,
241
+ ) -> None:
242
+ """Copy the completed temp file to its destination (receive side).
243
+
244
+ ``destination_filename`` from the :class:`Finalize` effect wins when the
245
+ machine resolved it from a late-recovered Metadata (Class-2 recovery);
246
+ otherwise fall back to the frozen transaction's filename (the Class-1
247
+ path, where Metadata arrives first).
248
+ """
249
+ if tid in self._direct_dest:
250
+ # Direct-to-destination mode: the data is already in the target
251
+ # file, so there is nothing to copy. Mark it committed so the
252
+ # following CloseFile keeps the file instead of deleting it.
253
+ self._direct_committed.add(tid)
254
+ return
255
+ dest_name = destination_filename or transaction.destination_filename
256
+ handle = self.files.get(tid)
257
+ if handle is None:
258
+ # No temp handle for this transaction (issue #30): raise a clean,
259
+ # descriptive error BEFORE opening dest, so the dispatcher's exception
260
+ # boundary faults cleanly rather than AttributeError-ing on ``None``
261
+ # (and no destination fd is opened, so none can leak).
262
+ raise RuntimeError(
263
+ f"cannot finalize {tid}: no open temp file handle to copy from"
264
+ )
265
+ dest = self.entity.filestore.open(dest_name, "wb")
266
+ try:
267
+ handle.seek(0)
268
+ chunk_size = 65536
269
+ while True:
270
+ chunk = handle.read(chunk_size)
271
+ if not chunk:
272
+ break
273
+ dest.write(chunk)
274
+ finally:
275
+ # Always close the destination handle, on every path (a mid-copy
276
+ # read/write error included), so a failed finalize never leaks an fd.
277
+ dest.close()
278
+
279
+ # -- indication fan-out ------------------------------------------------ #
280
+
281
+ def _emit_indication(self, effect):
282
+ method = getattr(self.entity, "_indication_" + effect.kind, None)
283
+ if method is None:
284
+ raise TypeError(f"unknown indication kind: {effect.kind!r}")
285
+ method(*effect.args)
@@ -0,0 +1,165 @@
1
+ """The ``tid -> machine`` registry and machine creation (design D7/D9).
2
+
3
+ Owns the map of live pure machines, builds them (folding config into a frozen
4
+ :class:`~cfdp.pure.config.TransactionConfig` snapshot, design D9, and injecting a
5
+ per-transaction :class:`~cfdp.shell.query_port.EntityQueryPort`, design D3), and
6
+ tracks completion for TTL reaping.
7
+
8
+ Two creation doors:
9
+
10
+ * :meth:`create_sender` — from a local ``put`` request.
11
+ * :meth:`create_receiver` — from an inbound Metadata/Filedata/EOF toward the
12
+ receiver.
13
+
14
+ Both Class-1 (``UNACKNOWLEDGED``) and Class-2 (``ACKNOWLEDGED``) pure machines
15
+ are built here (issue #20 wired Class-2 through the shell). These are the only
16
+ machines (the old machines were removed at the cutover, issue #22).
17
+ """
18
+
19
+ from ..constants import TransmissionMode
20
+ from ..pure import (
21
+ EntityDefaults,
22
+ PutOverrides,
23
+ TransactionConfig,
24
+ )
25
+ from ..pure import (
26
+ Transaction as PureTransaction,
27
+ )
28
+ from ..pure.machines import Receiver1 as PureReceiver1
29
+ from ..pure.machines import Receiver2 as PureReceiver2
30
+ from ..pure.machines import Sender1 as PureSender1
31
+ from ..pure.machines import Sender2 as PureSender2
32
+ from ..pure.transaction import TransactionId
33
+ from .query_port import EntityQueryPort
34
+
35
+
36
+ class MachineRegistry:
37
+ """The live pure-machine map plus creation and reaping (design D7)."""
38
+
39
+ def __init__(self, entity, file_table, machines=None):
40
+ self._entity = entity
41
+ self._files = file_table
42
+ # Share the entity's machines dict so every lookup path
43
+ # (cancel/suspend/resume/report/freeze/thaw, is_complete, reaping) sees
44
+ # the machines uniformly.
45
+ self.machines = machines if machines is not None else {}
46
+
47
+ # -- membership -------------------------------------------------------- #
48
+
49
+ def get(self, tid: TransactionId):
50
+ return self.machines.get(tid)
51
+
52
+ def __contains__(self, tid: TransactionId) -> bool:
53
+ return tid in self.machines
54
+
55
+ # -- config snapshot (design D9) --------------------------------------- #
56
+
57
+ def _entity_defaults(self):
58
+ return EntityDefaults(
59
+ transaction_inactivity_limit=self._entity.transaction_inactivity_limit,
60
+ ack_timer_interval=self._entity.positive_ack_timer_interval,
61
+ ack_timer_expiration_limit=self._entity.positive_ack_timer_expiration_limit,
62
+ nak_timer_interval=self._entity.nak_timer_interval,
63
+ nak_timer_expiration_limit=self._entity.nak_timer_expiration_limit,
64
+ maximum_file_segment_length=self._entity.maximum_file_segment_length,
65
+ inter_pdu_delay=self._entity.inter_pdu_delay,
66
+ fault_handlers=dict(self._entity.default_fault_handlers),
67
+ )
68
+
69
+ def _resolve_config(self, remote_config, put_overrides=None):
70
+ return TransactionConfig.resolve(
71
+ self._entity_defaults(), remote_config, put_overrides
72
+ )
73
+
74
+ # -- creation doors ---------------------------------------------------- #
75
+
76
+ def create_sender(
77
+ self,
78
+ *,
79
+ seq_number,
80
+ destination_id,
81
+ source_filename,
82
+ destination_filename,
83
+ checksum_type,
84
+ transmission_mode,
85
+ maximum_file_segment_length,
86
+ messages_to_user=None,
87
+ filestore_requests=None,
88
+ fault_handler_overrides=None,
89
+ remote_config=None,
90
+ ):
91
+ """Build a pure sender for a local ``put`` and register it.
92
+
93
+ Picks :class:`PureSender1` for ``UNACKNOWLEDGED`` and :class:`PureSender2`
94
+ for ``ACKNOWLEDGED`` (issue #20); both take the same
95
+ ``(transaction, config, query_port, messages_to_user, filestore_requests)``
96
+ constructor.
97
+ """
98
+ transaction = PureTransaction(
99
+ source_entity_id=self._entity.entity_id,
100
+ seq_number=seq_number,
101
+ destination_entity_id=destination_id,
102
+ transmission_mode=transmission_mode,
103
+ source_filename=source_filename,
104
+ destination_filename=destination_filename,
105
+ checksum_type=checksum_type,
106
+ )
107
+ put_overrides = PutOverrides(
108
+ maximum_file_segment_length=maximum_file_segment_length,
109
+ fault_handler_overrides=fault_handler_overrides,
110
+ )
111
+ config = self._resolve_config(remote_config, put_overrides)
112
+ query_port = EntityQueryPort(self._entity, transaction, self._files)
113
+ sender_cls = (
114
+ PureSender2
115
+ if transmission_mode == TransmissionMode.ACKNOWLEDGED
116
+ else PureSender1
117
+ )
118
+ machine = sender_cls(
119
+ transaction,
120
+ config,
121
+ query_port,
122
+ messages_to_user=messages_to_user,
123
+ filestore_requests=filestore_requests,
124
+ )
125
+ self.machines[transaction.id] = machine
126
+ return machine, transaction
127
+
128
+ def create_receiver(self, pdu, remote_config=None):
129
+ """Build a pure receiver from an inbound toward-receiver PDU.
130
+
131
+ Picks :class:`PureReceiver1` for ``UNACKNOWLEDGED`` and
132
+ :class:`PureReceiver2` for ``ACKNOWLEDGED`` (issue #20), keyed off the
133
+ inbound PDU header's transmission mode.
134
+ """
135
+ source_entity_id = pdu.pdu_header.source_entity_id
136
+ seq_number = pdu.pdu_header.transaction_seq_number
137
+ destination_entity_id = pdu.pdu_header.destination_entity_id
138
+ transmission_mode = pdu.pdu_header.transmission_mode
139
+ # Metadata carries the filenames + checksum type; Filedata/EOF-first do
140
+ # not, matching the old core._run_action_for_incoming_pdu split.
141
+ source_filename = getattr(pdu, "source_filename", None)
142
+ destination_filename = getattr(pdu, "destination_filename", None)
143
+ checksum_type = getattr(pdu, "checksum_type", None)
144
+ kwargs = {}
145
+ if checksum_type is not None:
146
+ kwargs["checksum_type"] = checksum_type
147
+ transaction = PureTransaction(
148
+ source_entity_id=source_entity_id,
149
+ seq_number=seq_number,
150
+ destination_entity_id=destination_entity_id,
151
+ transmission_mode=transmission_mode,
152
+ source_filename=source_filename,
153
+ destination_filename=destination_filename,
154
+ **kwargs,
155
+ )
156
+ config = self._resolve_config(remote_config)
157
+ query_port = EntityQueryPort(self._entity, transaction, self._files)
158
+ receiver_cls = (
159
+ PureReceiver2
160
+ if transmission_mode == TransmissionMode.ACKNOWLEDGED
161
+ else PureReceiver1
162
+ )
163
+ machine = receiver_cls(transaction, config, query_port)
164
+ self.machines[transaction.id] = machine
165
+ return machine, transaction
@@ -0,0 +1,70 @@
1
+ """The synchronous, local-only query port implementation (design D3/D5/D10).
2
+
3
+ A pure machine calls its injected :class:`~cfdp.pure.query_port.QueryPort`
4
+ synchronously mid-decision for local + fast reads only: source size/checksum,
5
+ sequential source segments, temp-file checksum read-back, and transport
6
+ readiness. :class:`EntityQueryPort` backs those against the entity's real
7
+ filestore/transport and the shell-held file handle (the send-side source handle
8
+ or the receive-side temp handle) in the :class:`~cfdp.shell.effect_executor.FileTable`.
9
+
10
+ One port instance is bound per transaction: it captures the ``tid`` and the
11
+ frozen :class:`~cfdp.pure.transaction.Transaction` so the machine's calls resolve
12
+ to that transaction's filenames and handle.
13
+ """
14
+
15
+ from ..pure.transaction import Transaction
16
+
17
+
18
+ class EntityQueryPort:
19
+ """A :class:`~cfdp.pure.query_port.QueryPort` bound to one transaction.
20
+
21
+ ``read_segment``/temp ``checksum`` read the handle the
22
+ :class:`~cfdp.shell.effect_executor.EffectExecutor` opened for this ``tid``
23
+ (design D10: the shell owns the fd, the machine holds only positions).
24
+ ``size``/source ``checksum`` open the source afresh, mirroring the old
25
+ ``Transaction`` read helpers exactly.
26
+ """
27
+
28
+ def __init__(self, entity, transaction: Transaction, file_table) -> None:
29
+ self._entity = entity
30
+ self._transaction = transaction
31
+ self._files = file_table
32
+ self._tid = transaction.id
33
+
34
+ def is_file(self, filename: str) -> bool:
35
+ return self._entity.filestore.is_file(filename)
36
+
37
+ def size(self, filename: str) -> int:
38
+ try:
39
+ return self._entity.filestore.get_size(filename)
40
+ except FileNotFoundError:
41
+ return 0
42
+
43
+ def checksum(self, checksum_type: int) -> int:
44
+ """Checksum the transaction's current file.
45
+
46
+ Receive side: read back the shell-held temp handle (design D10). Send
47
+ side (no temp handle in the table): open the source afresh, exactly as
48
+ the old ``Transaction.get_file_checksum`` did.
49
+ """
50
+ handle = self._files.get(self._tid)
51
+ if handle is not None:
52
+ return handle.calculate_checksum(checksum_type)
53
+ source = self._transaction.source_filename
54
+ if not source:
55
+ return 0
56
+ fh = self._entity.filestore.open(source)
57
+ try:
58
+ return fh.calculate_checksum(checksum_type)
59
+ finally:
60
+ fh.close()
61
+
62
+ def read_segment(self, offset: int, length: int) -> tuple[int, bytes]:
63
+ handle = self._files.get(self._tid)
64
+ if handle is None:
65
+ raise ValueError("File handle not defined")
66
+ handle.seek(offset)
67
+ return offset, handle.read(length)
68
+
69
+ def transport_ready(self) -> bool:
70
+ return self._entity.transport.is_ready()