zeroconf 0.152.0__cp313-cp313-win_arm64.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 (74) hide show
  1. zeroconf/__init__.py +104 -0
  2. zeroconf/_cache.cp313-win_arm64.pyd +0 -0
  3. zeroconf/_cache.pxd +96 -0
  4. zeroconf/_cache.py +316 -0
  5. zeroconf/_core.py +791 -0
  6. zeroconf/_dns.cp313-win_arm64.pyd +0 -0
  7. zeroconf/_dns.pxd +160 -0
  8. zeroconf/_dns.py +622 -0
  9. zeroconf/_engine.py +456 -0
  10. zeroconf/_exceptions.py +54 -0
  11. zeroconf/_handlers/__init__.py +8 -0
  12. zeroconf/_handlers/answers.cp313-win_arm64.pyd +0 -0
  13. zeroconf/_handlers/answers.pxd +34 -0
  14. zeroconf/_handlers/answers.py +110 -0
  15. zeroconf/_handlers/multicast_outgoing_queue.cp313-win_arm64.pyd +0 -0
  16. zeroconf/_handlers/multicast_outgoing_queue.pxd +27 -0
  17. zeroconf/_handlers/multicast_outgoing_queue.py +115 -0
  18. zeroconf/_handlers/query_handler.cp313-win_arm64.pyd +0 -0
  19. zeroconf/_handlers/query_handler.pxd +124 -0
  20. zeroconf/_handlers/query_handler.py +458 -0
  21. zeroconf/_handlers/record_manager.cp313-win_arm64.pyd +0 -0
  22. zeroconf/_handlers/record_manager.pxd +42 -0
  23. zeroconf/_handlers/record_manager.py +188 -0
  24. zeroconf/_history.cp313-win_arm64.pyd +0 -0
  25. zeroconf/_history.pxd +23 -0
  26. zeroconf/_history.py +93 -0
  27. zeroconf/_listener.cp313-win_arm64.pyd +0 -0
  28. zeroconf/_listener.pxd +63 -0
  29. zeroconf/_listener.py +353 -0
  30. zeroconf/_logger.py +95 -0
  31. zeroconf/_protocol/__init__.py +8 -0
  32. zeroconf/_protocol/incoming.cp313-win_arm64.pyd +0 -0
  33. zeroconf/_protocol/incoming.pxd +157 -0
  34. zeroconf/_protocol/incoming.py +571 -0
  35. zeroconf/_protocol/outgoing.cp313-win_arm64.pyd +0 -0
  36. zeroconf/_protocol/outgoing.pxd +143 -0
  37. zeroconf/_protocol/outgoing.py +481 -0
  38. zeroconf/_record_update.cp313-win_arm64.pyd +0 -0
  39. zeroconf/_record_update.pxd +12 -0
  40. zeroconf/_record_update.py +33 -0
  41. zeroconf/_services/__init__.cp313-win_arm64.pyd +0 -0
  42. zeroconf/_services/__init__.pxd +11 -0
  43. zeroconf/_services/__init__.py +63 -0
  44. zeroconf/_services/browser.cp313-win_arm64.pyd +0 -0
  45. zeroconf/_services/browser.pxd +122 -0
  46. zeroconf/_services/browser.py +810 -0
  47. zeroconf/_services/info.cp313-win_arm64.pyd +0 -0
  48. zeroconf/_services/info.pxd +197 -0
  49. zeroconf/_services/info.py +1138 -0
  50. zeroconf/_services/registry.cp313-win_arm64.pyd +0 -0
  51. zeroconf/_services/registry.pxd +37 -0
  52. zeroconf/_services/registry.py +108 -0
  53. zeroconf/_services/types.py +69 -0
  54. zeroconf/_transport.py +114 -0
  55. zeroconf/_updates.cp313-win_arm64.pyd +0 -0
  56. zeroconf/_updates.pxd +9 -0
  57. zeroconf/_updates.py +66 -0
  58. zeroconf/_utils/__init__.py +8 -0
  59. zeroconf/_utils/asyncio.py +126 -0
  60. zeroconf/_utils/ipaddress.cp313-win_arm64.pyd +0 -0
  61. zeroconf/_utils/ipaddress.pxd +16 -0
  62. zeroconf/_utils/ipaddress.py +148 -0
  63. zeroconf/_utils/name.py +166 -0
  64. zeroconf/_utils/net.py +545 -0
  65. zeroconf/_utils/time.cp313-win_arm64.pyd +0 -0
  66. zeroconf/_utils/time.pxd +4 -0
  67. zeroconf/_utils/time.py +26 -0
  68. zeroconf/asyncio.py +248 -0
  69. zeroconf/const.py +193 -0
  70. zeroconf/py.typed +0 -0
  71. zeroconf-0.152.0.dist-info/METADATA +177 -0
  72. zeroconf-0.152.0.dist-info/RECORD +74 -0
  73. zeroconf-0.152.0.dist-info/WHEEL +4 -0
  74. zeroconf-0.152.0.dist-info/licenses/COPYING +475 -0
zeroconf/__init__.py ADDED
@@ -0,0 +1,104 @@
1
+ """A pure python implementation of multicast DNS service discovery.
2
+
3
+ Licensed under LGPL-2.1-or-later; see COPYING for details. This file is
4
+ part of a continuously modified work; modification dates are recorded
5
+ in the project's git history.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from ._cache import DNSCache # noqa # import needed for backwards compat
11
+ from ._core import Zeroconf
12
+ from ._dns import ( # noqa # import needed for backwards compat
13
+ DNSAddress,
14
+ DNSEntry,
15
+ DNSHinfo,
16
+ DNSNsec,
17
+ DNSPointer,
18
+ DNSQuestion,
19
+ DNSQuestionType,
20
+ DNSRecord,
21
+ DNSService,
22
+ DNSText,
23
+ )
24
+ from ._exceptions import (
25
+ AbstractMethodException,
26
+ BadTypeInNameException,
27
+ Error,
28
+ EventLoopBlocked,
29
+ IncomingDecodeError,
30
+ NamePartTooLongException,
31
+ NonUniqueNameException,
32
+ NotRunningException,
33
+ ServiceNameAlreadyRegistered,
34
+ )
35
+ from ._logger import QuietLogger, log # noqa # import needed for backwards compat
36
+ from ._protocol.incoming import DNSIncoming # noqa # import needed for backwards compat
37
+ from ._protocol.outgoing import DNSOutgoing # noqa # import needed for backwards compat
38
+ from ._record_update import RecordUpdate
39
+ from ._services import ( # noqa # import needed for backwards compat
40
+ ServiceListener,
41
+ ServiceStateChange,
42
+ Signal,
43
+ SignalRegistrationInterface,
44
+ )
45
+ from ._services.browser import ServiceBrowser
46
+ from ._services.info import ( # noqa # import needed for backwards compat
47
+ AddressResolver,
48
+ AddressResolverIPv4,
49
+ AddressResolverIPv6,
50
+ ServiceInfo,
51
+ instance_name_from_service_info,
52
+ )
53
+ from ._services.registry import ( # noqa # import needed for backwards compat
54
+ ServiceRegistry,
55
+ )
56
+ from ._services.types import ZeroconfServiceTypes
57
+ from ._updates import RecordUpdateListener
58
+ from ._utils.name import service_type_name # noqa # import needed for backwards compat
59
+ from ._utils.net import ( # noqa # import needed for backwards compat
60
+ InterfaceChoice,
61
+ InterfacesType,
62
+ IPVersion,
63
+ add_multicast_member,
64
+ autodetect_ip_version,
65
+ create_sockets,
66
+ get_all_addresses,
67
+ get_all_addresses_v6,
68
+ )
69
+ from ._utils.time import ( # noqa # import needed for backwards compat
70
+ current_time_millis,
71
+ millis_to_seconds,
72
+ )
73
+
74
+ __author__ = "The python-zeroconf authors" # full history in COPYING and git
75
+ __maintainer__ = "J. Nick Koston <nick@koston.org>"
76
+ __version__ = "0.152.0"
77
+ __license__ = "LGPL"
78
+
79
+
80
+ __all__ = [
81
+ "AbstractMethodException",
82
+ "BadTypeInNameException",
83
+ "DNSQuestionType",
84
+ # Exceptions
85
+ "Error",
86
+ "EventLoopBlocked",
87
+ "IPVersion",
88
+ "IncomingDecodeError",
89
+ "InterfaceChoice",
90
+ "NamePartTooLongException",
91
+ "NonUniqueNameException",
92
+ "NotRunningException",
93
+ "RecordUpdate",
94
+ "RecordUpdateListener",
95
+ "ServiceBrowser",
96
+ "ServiceInfo",
97
+ "ServiceListener",
98
+ "ServiceNameAlreadyRegistered",
99
+ "ServiceStateChange",
100
+ "Zeroconf",
101
+ "ZeroconfServiceTypes",
102
+ "__version__",
103
+ "current_time_millis",
104
+ ]
Binary file
zeroconf/_cache.pxd ADDED
@@ -0,0 +1,96 @@
1
+ import cython
2
+
3
+ from ._dns cimport (
4
+ DNSAddress,
5
+ DNSEntry,
6
+ DNSHinfo,
7
+ DNSNsec,
8
+ DNSPointer,
9
+ DNSRecord,
10
+ DNSService,
11
+ DNSText,
12
+ )
13
+
14
+ cdef object heappop
15
+ cdef object heappush
16
+ cdef object heapify
17
+
18
+ cdef object _UNIQUE_RECORD_TYPES
19
+ cdef unsigned int _TYPE_PTR
20
+ cdef cython.uint _ONE_SECOND
21
+ cdef unsigned int _MIN_SCHEDULED_RECORD_EXPIRATION
22
+ cdef unsigned int _MAX_CACHE_RECORDS
23
+
24
+
25
+ @cython.locals(record_cache=dict)
26
+ cdef _remove_key(cython.dict cache, object key, DNSRecord record)
27
+
28
+
29
+ cdef class DNSCache:
30
+
31
+ cdef public cython.dict cache
32
+ cdef public cython.dict service_cache
33
+ cdef public list _expire_heap
34
+ cdef public dict _expirations
35
+ cdef public unsigned int _total_records
36
+
37
+ cpdef bint async_add_records(self, object entries)
38
+
39
+ cpdef void async_remove_records(self, object entries)
40
+
41
+ @cython.locals(store=cython.dict)
42
+ cpdef DNSRecord async_get_unique(self, DNSRecord entry)
43
+
44
+ @cython.locals(record=DNSRecord, when_record=tuple, when=double)
45
+ cpdef list async_expire(self, double now)
46
+
47
+ @cython.locals(records=cython.dict, record=DNSRecord)
48
+ cpdef list async_all_by_details(self, str name, unsigned int type_, unsigned int class_)
49
+
50
+ cpdef list async_entries_with_name(self, str name)
51
+
52
+ cpdef list async_entries_with_server(self, str name)
53
+
54
+ @cython.locals(cached_entry=DNSRecord, records=dict)
55
+ cpdef DNSRecord get_by_details(self, str name, unsigned int type_, unsigned int class_)
56
+
57
+ @cython.locals(records=cython.dict, entry=DNSRecord)
58
+ cpdef cython.list get_all_by_details(self, str name, unsigned int type_, unsigned int class_)
59
+
60
+ @cython.locals(
61
+ store=cython.dict,
62
+ service_store=cython.dict,
63
+ service_record=DNSService,
64
+ when=object,
65
+ new=bint,
66
+ is_new=bint
67
+ )
68
+ cdef bint _async_add(self, DNSRecord record)
69
+
70
+ @cython.locals(record=DNSRecord, when_record=tuple)
71
+ cdef void _async_evict_oldest(self)
72
+
73
+ @cython.locals(expire_heap_len="unsigned int")
74
+ cdef void _maybe_rebuild_heap(self)
75
+
76
+ @cython.locals(service_record=DNSService)
77
+ cdef void _async_remove(self, DNSRecord record)
78
+
79
+ @cython.locals(record=DNSRecord, created_double=double)
80
+ cpdef void async_mark_unique_records_older_than_1s_to_expire(self, cython.set unique_types, object answers, double now)
81
+
82
+ @cython.locals(entries=dict)
83
+ cpdef list entries_with_name(self, str name)
84
+
85
+ @cython.locals(entries=dict)
86
+ cpdef list entries_with_server(self, str server)
87
+
88
+ @cython.locals(record=DNSRecord, now=double)
89
+ cpdef current_entry_with_name_and_alias(self, str name, str alias)
90
+
91
+ cpdef void _async_set_created_ttl(
92
+ self,
93
+ DNSRecord record,
94
+ double now,
95
+ unsigned int ttl
96
+ )
zeroconf/_cache.py ADDED
@@ -0,0 +1,316 @@
1
+ """A pure python implementation of multicast DNS service discovery.
2
+
3
+ Licensed under LGPL-2.1-or-later; see COPYING for details. This file is
4
+ part of a continuously modified work; modification dates are recorded
5
+ in the project's git history.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable
11
+ from heapq import heapify, heappop, heappush
12
+ from typing import cast
13
+
14
+ from ._dns import (
15
+ DNSAddress,
16
+ DNSEntry,
17
+ DNSHinfo,
18
+ DNSNsec,
19
+ DNSPointer,
20
+ DNSRecord,
21
+ DNSService,
22
+ DNSText,
23
+ )
24
+ from ._utils.time import current_time_millis
25
+ from .const import _MAX_CACHE_RECORDS, _ONE_SECOND, _TYPE_PTR
26
+
27
+ _UNIQUE_RECORD_TYPES = (DNSAddress, DNSHinfo, DNSPointer, DNSText, DNSService)
28
+ _UniqueRecordsType = DNSAddress | DNSHinfo | DNSPointer | DNSText | DNSService
29
+ _DNSRecordCacheType = dict[str, dict[DNSRecord, DNSRecord]]
30
+ _UniqueType = tuple[str, int, int]
31
+ _DNSRecord = DNSRecord
32
+ _str = str
33
+ _float = float
34
+ _int = int
35
+
36
+ # The minimum number of scheduled record expirations before we start cleaning up
37
+ # the expiration heap. This is a performance optimization to avoid cleaning up the
38
+ # heap too often when there are only a few scheduled expirations.
39
+ _MIN_SCHEDULED_RECORD_EXPIRATION = 100
40
+
41
+
42
+ def _remove_key(cache: _DNSRecordCacheType, key: _str, record: _DNSRecord) -> None:
43
+ """Remove a key from a DNSRecord cache
44
+
45
+ This function must be run in from event loop.
46
+ """
47
+ record_cache = cache[key]
48
+ del record_cache[record]
49
+ if not record_cache:
50
+ del cache[key]
51
+
52
+
53
+ class DNSCache:
54
+ """Store of received DNS records, indexed for fast lookup."""
55
+
56
+ def __init__(self) -> None:
57
+ self.cache: _DNSRecordCacheType = {}
58
+ self._expire_heap: list[tuple[float, DNSRecord]] = []
59
+ self._expirations: dict[DNSRecord, float] = {}
60
+ self.service_cache: _DNSRecordCacheType = {}
61
+ self._total_records: int = 0
62
+
63
+ # Functions prefixed with async_ are NOT threadsafe and must
64
+ # be run in the event loop.
65
+
66
+ def async_add_records(self, entries: Iterable[DNSRecord]) -> bool:
67
+ """Add multiple records.
68
+
69
+ Returns true if any of the records were not in the cache.
70
+
71
+ This function must be run in from event loop.
72
+ """
73
+ new = False
74
+ for entry in entries:
75
+ if self._async_add(entry):
76
+ new = True
77
+ return new
78
+
79
+ def async_all_by_details(self, name: _str, type_: _int, class_: _int) -> list[DNSRecord]:
80
+ """Return every cached record carrying this name, type and class.
81
+
82
+ This function is not thread-safe and must be called from
83
+ the event loop.
84
+ """
85
+ key = name.lower()
86
+ records = self.cache.get(key)
87
+ matches: list[DNSRecord] = []
88
+ if records is None:
89
+ return matches
90
+ for record in records.values():
91
+ if type_ == record.type and class_ == record.class_:
92
+ matches.append(record)
93
+ return matches
94
+
95
+ def async_entries_with_name(self, name: str) -> list[DNSRecord]:
96
+ """All cached records for a name; event loop only, not threadsafe."""
97
+ return self.entries_with_name(name)
98
+
99
+ def async_entries_with_server(self, name: str) -> list[DNSRecord]:
100
+ """All cached records for a server name; event loop only, not threadsafe."""
101
+ return self.entries_with_server(name)
102
+
103
+ # The below functions are threadsafe and do not need to be run in the
104
+ # event loop, however they all make copies so they significantly
105
+ # inefficient.
106
+
107
+ def async_expire(self, now: _float) -> list[DNSRecord]:
108
+ """Purge expired entries from the cache.
109
+
110
+ This function must be run in from event loop.
111
+
112
+ :param now: The current time in milliseconds.
113
+ """
114
+ if not self._expire_heap:
115
+ return []
116
+
117
+ expired: list[DNSRecord] = []
118
+ while self._expire_heap:
119
+ when_record = self._expire_heap[0]
120
+ when = when_record[0]
121
+ if when > now:
122
+ break
123
+ heappop(self._expire_heap)
124
+ # Skip entries left behind by a TTL re-add; the live tuple is
125
+ # later in the heap and will be removed when it reaches the top.
126
+ record = when_record[1]
127
+ if self._expirations.get(record) == when:
128
+ expired.append(record)
129
+
130
+ self._maybe_rebuild_heap()
131
+ self.async_remove_records(expired)
132
+ return expired
133
+
134
+ def async_get_unique(self, entry: _UniqueRecordsType) -> DNSRecord | None:
135
+ """Look up the cached copy of a unique record, or None.
136
+
137
+ Event loop only; not threadsafe.
138
+ """
139
+ store = self.cache.get(entry.key)
140
+ if store is None:
141
+ return None
142
+ return store.get(entry)
143
+
144
+ def async_mark_unique_records_older_than_1s_to_expire(
145
+ self,
146
+ unique_types: set[_UniqueType],
147
+ answers: Iterable[DNSRecord],
148
+ now: _float,
149
+ ) -> None:
150
+ # rfc6762#section-10.2 para 2
151
+ # Since unique is set, all old records with that name, rrtype,
152
+ # and rrclass that were received more than one second ago are declared
153
+ # invalid, and marked to expire from the cache in one second.
154
+ answers_rrset = set(answers)
155
+ for name, type_, class_ in unique_types:
156
+ for record in self.async_all_by_details(name, type_, class_):
157
+ created_double = record.created
158
+ if (now - created_double > _ONE_SECOND) and record not in answers_rrset:
159
+ # Expire in 1s
160
+ self._async_set_created_ttl(record, now, 1)
161
+
162
+ def async_remove_records(self, entries: Iterable[DNSRecord]) -> None:
163
+ """Remove multiple records.
164
+
165
+ This function must be run in from event loop.
166
+ """
167
+ for entry in entries:
168
+ self._async_remove(entry)
169
+
170
+ def current_entry_with_name_and_alias(self, name: str, alias: str) -> DNSRecord | None:
171
+ now = current_time_millis()
172
+ for record in reversed(self.entries_with_name(name)):
173
+ if (
174
+ record.type == _TYPE_PTR
175
+ and not record.is_expired(now)
176
+ and cast(DNSPointer, record).alias == alias
177
+ ):
178
+ return record
179
+ return None
180
+
181
+ def entries_with_name(self, name: str) -> list[DNSRecord]:
182
+ if entries := self.cache.get(name.lower()):
183
+ return list(entries.values())
184
+ return []
185
+
186
+ def entries_with_server(self, server: str) -> list[DNSRecord]:
187
+ """All cached records whose server field matches."""
188
+ if entries := self.service_cache.get(server.lower()):
189
+ return list(entries.values())
190
+ return []
191
+
192
+ def get(self, entry: DNSEntry) -> DNSRecord | None:
193
+ if isinstance(entry, _UNIQUE_RECORD_TYPES):
194
+ return self.cache.get(entry.key, {}).get(entry)
195
+ for cached_entry in reversed(list(self.cache.get(entry.key, {}).values())):
196
+ if entry.__eq__(cached_entry):
197
+ return cached_entry
198
+ return None
199
+
200
+ def get_all_by_details(self, name: str, type_: _int, class_: _int) -> list[DNSRecord]:
201
+ """Return every cached record carrying this name, type and class."""
202
+ key = name.lower()
203
+ records = self.cache.get(key)
204
+ if records is None:
205
+ return []
206
+ return [entry for entry in list(records.values()) if type_ == entry.type and class_ == entry.class_]
207
+
208
+ def get_by_details(self, name: str, type_: _int, class_: _int) -> DNSRecord | None:
209
+ """Return the first cached record carrying this name, type and class, or None.
210
+
211
+ Calling this function is not recommended as it will only
212
+ return one record even if there are multiple entries.
213
+
214
+ For example if there are multiple A or AAAA addresses this
215
+ function will return the last one that was added to the cache
216
+ which may not be the one you expect.
217
+
218
+ Use get_all_by_details instead.
219
+ """
220
+ key = name.lower()
221
+ records = self.cache.get(key)
222
+ if records is None:
223
+ return None
224
+ for cached_entry in reversed(list(records.values())):
225
+ if type_ == cached_entry.type and class_ == cached_entry.class_:
226
+ return cached_entry
227
+ return None
228
+
229
+ def names(self) -> list[str]:
230
+ """Return a copy of the list of current cache names."""
231
+ return list(self.cache)
232
+
233
+ def _async_add(self, record: _DNSRecord) -> bool:
234
+ """Store a record, returning True when it was not cached before.
235
+
236
+ Event loop only; not threadsafe.
237
+ """
238
+ # Previously storage of records was implemented as a list
239
+ # instead a dict. Since DNSRecords are now hashable, the implementation
240
+ # uses a dict to ensure that adding a new record to the cache
241
+ # replaces any existing records that are __eq__ to each other which
242
+ # removes the risk that accessing the cache from the wrong
243
+ # direction would return the old incorrect entry.
244
+ store = self.cache.get(record.key)
245
+ is_new = store is None or record not in store
246
+ # Bound total cache size; evict closest-to-expiration entry to
247
+ # make room before inserting a new record. Prevents a LAN-local
248
+ # flood of unique-name records from growing the cache without
249
+ # bound (RFC 6762 §10 advisory caching, defense-in-depth).
250
+ if is_new and self._total_records >= _MAX_CACHE_RECORDS:
251
+ self._async_evict_oldest()
252
+ # The victim may have been the last record under
253
+ # ``record.key``, in which case ``_remove_key`` deleted
254
+ # the bucket. Re-fetch before creating below.
255
+ store = self.cache.get(record.key)
256
+ if store is None:
257
+ store = self.cache[record.key] = {}
258
+ new = is_new and not isinstance(record, DNSNsec)
259
+ if is_new:
260
+ self._total_records += 1
261
+ store[record] = record
262
+ when = record.created + (record.ttl * 1000)
263
+ if self._expirations.get(record) != when:
264
+ heappush(self._expire_heap, (when, record))
265
+ self._expirations[record] = when
266
+ # Re-adds of an existing record with a new TTL push a fresh
267
+ # entry but leave the prior tuple behind as stale, so a peer
268
+ # that just replays cached records can grow ``_expire_heap``
269
+ # without ever tripping the cap. Rebuild when stale entries
270
+ # dominate.
271
+ self._maybe_rebuild_heap()
272
+
273
+ if isinstance(record, DNSService):
274
+ service_record = record
275
+ if (service_store := self.service_cache.get(service_record.server_key)) is None:
276
+ service_store = self.service_cache[service_record.server_key] = {}
277
+ service_store[service_record] = service_record
278
+ return new
279
+
280
+ def _async_evict_oldest(self) -> None:
281
+ """Drop the closest-to-expiration record to make room for a new one."""
282
+ while self._expire_heap:
283
+ when_record = heappop(self._expire_heap)
284
+ record = when_record[1]
285
+ if self._expirations.get(record) != when_record[0]:
286
+ continue
287
+ self._async_remove(record)
288
+ return
289
+
290
+ def _async_remove(self, record: _DNSRecord) -> None:
291
+ """Drop a record from the cache. Event loop only; not threadsafe."""
292
+ if isinstance(record, DNSService):
293
+ service_record = record
294
+ _remove_key(self.service_cache, service_record.server_key, service_record)
295
+ _remove_key(self.cache, record.key, record)
296
+ self._expirations.pop(record, None)
297
+ self._total_records -= 1
298
+
299
+ def _async_set_created_ttl(self, record: DNSRecord, now: _float, ttl: _int) -> None:
300
+ """Stamp a record with a new ttl and creation moment, then cache it."""
301
+ # It would be better if we made a copy instead of mutating the record
302
+ # in place, but records currently don't have a copy method.
303
+ record._set_created_ttl(now, ttl)
304
+ self._async_add(record)
305
+
306
+ def _maybe_rebuild_heap(self) -> None:
307
+ """Rebuild ``_expire_heap`` when stale entries dominate live ones."""
308
+ expire_heap_len = len(self._expire_heap)
309
+ if (
310
+ expire_heap_len > _MIN_SCHEDULED_RECORD_EXPIRATION
311
+ and expire_heap_len > len(self._expirations) * 2
312
+ ):
313
+ self._expire_heap = [
314
+ entry for entry in self._expire_heap if self._expirations.get(entry[1]) == entry[0]
315
+ ]
316
+ heapify(self._expire_heap)