lacuscore 1.9.2__py3-none-any.whl → 1.9.4__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.
- lacuscore/__init__.py +2 -1
- lacuscore/helpers.py +102 -0
- lacuscore/lacuscore.py +36 -104
- lacuscore/task_logger.py +59 -0
- {lacuscore-1.9.2.dist-info → lacuscore-1.9.4.dist-info}/METADATA +4 -4
- lacuscore-1.9.4.dist-info/RECORD +10 -0
- lacuscore-1.9.2.dist-info/RECORD +0 -8
- {lacuscore-1.9.2.dist-info → lacuscore-1.9.4.dist-info}/LICENSE +0 -0
- {lacuscore-1.9.2.dist-info → lacuscore-1.9.4.dist-info}/WHEEL +0 -0
lacuscore/__init__.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
from .lacuscore import LacusCore
|
1
|
+
from .lacuscore import LacusCore
|
2
|
+
from .helpers import CaptureStatus, CaptureResponse, CaptureResponseJson, CaptureSettings # noqa
|
2
3
|
from .lacus_monitoring import LacusCoreMonitoring # noqa
|
3
4
|
|
4
5
|
__all__ = [
|
lacuscore/helpers.py
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from enum import IntEnum, unique
|
6
|
+
from logging import LoggerAdapter
|
7
|
+
from typing import MutableMapping, Any, TypedDict
|
8
|
+
|
9
|
+
from playwrightcapture.capture import CaptureResponse as PlaywrightCaptureResponse
|
10
|
+
|
11
|
+
|
12
|
+
class LacusCoreException(Exception):
|
13
|
+
pass
|
14
|
+
|
15
|
+
|
16
|
+
class CaptureError(LacusCoreException):
|
17
|
+
pass
|
18
|
+
|
19
|
+
|
20
|
+
class RetryCapture(LacusCoreException):
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
class CaptureSettingsError(LacusCoreException):
|
25
|
+
pass
|
26
|
+
|
27
|
+
|
28
|
+
class LacusCoreLogAdapter(LoggerAdapter): # type: ignore[type-arg]
|
29
|
+
"""
|
30
|
+
Prepend log entry with the UUID of the capture
|
31
|
+
"""
|
32
|
+
def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]:
|
33
|
+
if self.extra:
|
34
|
+
return '[{}] {}'.format(self.extra['uuid'], msg), kwargs
|
35
|
+
return msg, kwargs
|
36
|
+
|
37
|
+
|
38
|
+
@unique
|
39
|
+
class CaptureStatus(IntEnum):
|
40
|
+
'''The status of the capture'''
|
41
|
+
UNKNOWN = -1
|
42
|
+
QUEUED = 0
|
43
|
+
DONE = 1
|
44
|
+
ONGOING = 2
|
45
|
+
|
46
|
+
|
47
|
+
class CaptureResponse(PlaywrightCaptureResponse, TypedDict, total=False):
|
48
|
+
'''A capture made by Lacus. With the base64 encoded image and downloaded file decoded to bytes.'''
|
49
|
+
|
50
|
+
# Need to make sure the type is what's expected down the line
|
51
|
+
children: list[CaptureResponse] | None # type: ignore[misc]
|
52
|
+
|
53
|
+
status: int
|
54
|
+
runtime: float | None
|
55
|
+
|
56
|
+
|
57
|
+
class CaptureResponseJson(TypedDict, total=False):
|
58
|
+
'''A capture made by Lacus. With the base64 encoded image and downloaded file *not* decoded.'''
|
59
|
+
|
60
|
+
status: int
|
61
|
+
last_redirected_url: str | None
|
62
|
+
har: dict[str, Any] | None
|
63
|
+
cookies: list[dict[str, str]] | None
|
64
|
+
error: str | None
|
65
|
+
html: str | None
|
66
|
+
png: str | None
|
67
|
+
downloaded_filename: str | None
|
68
|
+
downloaded_file: str | None
|
69
|
+
children: list[CaptureResponseJson] | None
|
70
|
+
runtime: float | None
|
71
|
+
potential_favicons: list[str] | None
|
72
|
+
|
73
|
+
|
74
|
+
class CaptureSettings(TypedDict, total=False):
|
75
|
+
'''The capture settings that can be passed to Lacus.'''
|
76
|
+
|
77
|
+
url: str | None
|
78
|
+
document_name: str | None
|
79
|
+
document: str | None
|
80
|
+
browser: str | None
|
81
|
+
device_name: str | None
|
82
|
+
user_agent: str | None
|
83
|
+
proxy: str | dict[str, str] | None
|
84
|
+
general_timeout_in_sec: int | None
|
85
|
+
cookies: list[dict[str, Any]] | None
|
86
|
+
headers: str | dict[str, str] | None
|
87
|
+
http_credentials: dict[str, str] | None
|
88
|
+
geolocation: dict[str, float] | None
|
89
|
+
timezone_id: str | None
|
90
|
+
locale: str | None
|
91
|
+
color_scheme: str | None
|
92
|
+
viewport: dict[str, int] | None
|
93
|
+
referer: str | None
|
94
|
+
with_favicon: bool
|
95
|
+
allow_tracking: bool
|
96
|
+
force: bool
|
97
|
+
recapture_interval: int
|
98
|
+
priority: int
|
99
|
+
uuid: str | None
|
100
|
+
|
101
|
+
depth: int
|
102
|
+
rendered_hostname_only: bool # Note: only used if depth is > 0
|
lacuscore/lacuscore.py
CHANGED
@@ -18,12 +18,10 @@ import zlib
|
|
18
18
|
from asyncio import Task
|
19
19
|
from base64 import b64decode, b64encode
|
20
20
|
from datetime import date, timedelta
|
21
|
-
from enum import IntEnum, unique
|
22
21
|
from ipaddress import ip_address, IPv4Address, IPv6Address
|
23
|
-
from logging import LoggerAdapter
|
24
22
|
from pathlib import Path
|
25
23
|
from tempfile import NamedTemporaryFile
|
26
|
-
from typing import Literal, Any,
|
24
|
+
from typing import Literal, Any, overload, cast, Iterator
|
27
25
|
from uuid import uuid4
|
28
26
|
from urllib.parse import urlsplit
|
29
27
|
|
@@ -33,17 +31,31 @@ from dns.exception import Timeout as DNSTimeout
|
|
33
31
|
|
34
32
|
from defang import refang # type: ignore[import-untyped]
|
35
33
|
from playwrightcapture import Capture, PlaywrightCaptureException
|
36
|
-
from playwrightcapture.capture import CaptureResponse as PlaywrightCaptureResponse
|
37
34
|
from redis import Redis
|
38
35
|
from redis.exceptions import ConnectionError as RedisConnectionError
|
39
36
|
from redis.exceptions import DataError
|
40
37
|
from ua_parser import user_agent_parser # type: ignore[import-untyped]
|
41
38
|
|
39
|
+
from . import task_logger
|
40
|
+
from .helpers import (
|
41
|
+
LacusCoreLogAdapter, CaptureError, RetryCapture, CaptureSettingsError,
|
42
|
+
CaptureStatus, CaptureResponse, CaptureResponseJson, CaptureSettings)
|
43
|
+
|
42
44
|
if sys.version_info < (3, 11):
|
43
45
|
from async_timeout import timeout
|
46
|
+
|
47
|
+
def timeout_expired(timeout_cm, logger, error_message: str) -> None: # type: ignore[no-untyped-def]
|
48
|
+
if timeout_cm.expired:
|
49
|
+
logger.warning(f'Timeout expired: {error_message}')
|
50
|
+
|
44
51
|
else:
|
45
52
|
from asyncio import timeout
|
46
53
|
|
54
|
+
def timeout_expired(timeout_cm, logger, error_message: str) -> None: # type: ignore[no-untyped-def]
|
55
|
+
if timeout_cm.expired():
|
56
|
+
logger.warning(f'Timeout expired: {error_message}')
|
57
|
+
|
58
|
+
|
47
59
|
BROWSER = Literal['chromium', 'firefox', 'webkit']
|
48
60
|
|
49
61
|
|
@@ -65,95 +77,6 @@ def _secure_filename(filename: str) -> str:
|
|
65
77
|
return filename
|
66
78
|
|
67
79
|
|
68
|
-
class LacusCoreException(Exception):
|
69
|
-
pass
|
70
|
-
|
71
|
-
|
72
|
-
class CaptureError(LacusCoreException):
|
73
|
-
pass
|
74
|
-
|
75
|
-
|
76
|
-
class RetryCapture(LacusCoreException):
|
77
|
-
pass
|
78
|
-
|
79
|
-
|
80
|
-
@unique
|
81
|
-
class CaptureStatus(IntEnum):
|
82
|
-
'''The status of the capture'''
|
83
|
-
UNKNOWN = -1
|
84
|
-
QUEUED = 0
|
85
|
-
DONE = 1
|
86
|
-
ONGOING = 2
|
87
|
-
|
88
|
-
|
89
|
-
class CaptureResponse(PlaywrightCaptureResponse, TypedDict, total=False):
|
90
|
-
'''A capture made by Lacus. With the base64 encoded image and downloaded file decoded to bytes.'''
|
91
|
-
|
92
|
-
# Need to make sure the type is what's expected down the line
|
93
|
-
children: list[CaptureResponse] | None # type: ignore[misc]
|
94
|
-
|
95
|
-
status: int
|
96
|
-
runtime: float | None
|
97
|
-
|
98
|
-
|
99
|
-
class CaptureResponseJson(TypedDict, total=False):
|
100
|
-
'''A capture made by Lacus. With the base64 encoded image and downloaded file *not* decoded.'''
|
101
|
-
|
102
|
-
status: int
|
103
|
-
last_redirected_url: str | None
|
104
|
-
har: dict[str, Any] | None
|
105
|
-
cookies: list[dict[str, str]] | None
|
106
|
-
error: str | None
|
107
|
-
html: str | None
|
108
|
-
png: str | None
|
109
|
-
downloaded_filename: str | None
|
110
|
-
downloaded_file: str | None
|
111
|
-
children: list[CaptureResponseJson] | None
|
112
|
-
runtime: float | None
|
113
|
-
potential_favicons: list[str] | None
|
114
|
-
|
115
|
-
|
116
|
-
class CaptureSettings(TypedDict, total=False):
|
117
|
-
'''The capture settings that can be passed to Lacus.'''
|
118
|
-
|
119
|
-
url: str | None
|
120
|
-
document_name: str | None
|
121
|
-
document: str | None
|
122
|
-
browser: str | None
|
123
|
-
device_name: str | None
|
124
|
-
user_agent: str | None
|
125
|
-
proxy: str | dict[str, str] | None
|
126
|
-
general_timeout_in_sec: int | None
|
127
|
-
cookies: list[dict[str, Any]] | None
|
128
|
-
headers: str | dict[str, str] | None
|
129
|
-
http_credentials: dict[str, str] | None
|
130
|
-
geolocation: dict[str, float] | None
|
131
|
-
timezone_id: str | None
|
132
|
-
locale: str | None
|
133
|
-
color_scheme: str | None
|
134
|
-
viewport: dict[str, int] | None
|
135
|
-
referer: str | None
|
136
|
-
with_favicon: bool
|
137
|
-
allow_tracking: bool
|
138
|
-
force: bool
|
139
|
-
recapture_interval: int
|
140
|
-
priority: int
|
141
|
-
uuid: str | None
|
142
|
-
|
143
|
-
depth: int
|
144
|
-
rendered_hostname_only: bool # Note: only used if depth is > 0
|
145
|
-
|
146
|
-
|
147
|
-
class LacusCoreLogAdapter(LoggerAdapter): # type: ignore[type-arg]
|
148
|
-
"""
|
149
|
-
Prepend log entry with the UUID of the capture
|
150
|
-
"""
|
151
|
-
def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]:
|
152
|
-
if self.extra:
|
153
|
-
return '[{}] {}'.format(self.extra['uuid'], msg), kwargs
|
154
|
-
return msg, kwargs
|
155
|
-
|
156
|
-
|
157
80
|
class LacusCore():
|
158
81
|
"""Capture URLs or web enabled documents using PlaywrightCapture.
|
159
82
|
|
@@ -368,7 +291,7 @@ class LacusCore():
|
|
368
291
|
p.execute()
|
369
292
|
except DataError:
|
370
293
|
self.master_logger.exception(f'Unable to enqueue: {to_enqueue}')
|
371
|
-
raise
|
294
|
+
raise CaptureSettingsError(f'Unable to enqueue: {to_enqueue}')
|
372
295
|
return perma_uuid
|
373
296
|
|
374
297
|
def _encode_response(self, capture: CaptureResponse) -> CaptureResponseJson:
|
@@ -453,7 +376,10 @@ class LacusCore():
|
|
453
376
|
max_consume -= 1
|
454
377
|
uuid: str = value[0][0].decode()
|
455
378
|
priority: int = int(value[0][1])
|
456
|
-
|
379
|
+
logger = LacusCoreLogAdapter(self.master_logger, {'uuid': uuid})
|
380
|
+
yield task_logger.create_task(self._capture(uuid, priority), name=uuid,
|
381
|
+
logger=logger,
|
382
|
+
message='Capture raised an uncaught exception')
|
457
383
|
|
458
384
|
async def _capture(self, uuid: str, priority: int) -> None:
|
459
385
|
"""Trigger a specific capture
|
@@ -508,11 +434,11 @@ class LacusCore():
|
|
508
434
|
elif k == 'document':
|
509
435
|
document_as_bytes = b64decode(v)
|
510
436
|
else:
|
511
|
-
raise
|
512
|
-
except
|
437
|
+
raise CaptureSettingsError(f'Unexpected setting: {k}: {v}')
|
438
|
+
except CaptureSettingsError as e:
|
513
439
|
raise e
|
514
440
|
except Exception as e:
|
515
|
-
raise
|
441
|
+
raise CaptureSettingsError(f'Error while preparing settings: {e}')
|
516
442
|
|
517
443
|
if not to_capture:
|
518
444
|
all_entries = self.redis.hgetall(f'lacus:capture_settings:{uuid}')
|
@@ -523,7 +449,7 @@ class LacusCore():
|
|
523
449
|
# we do not have a URL yet.
|
524
450
|
name = to_capture.pop('document_name', None)
|
525
451
|
if not name:
|
526
|
-
raise
|
452
|
+
raise CaptureSettingsError('No document name provided, settings are invalid')
|
527
453
|
if not Path(name).suffix:
|
528
454
|
# The browser will simply display the file as text if there is no extension.
|
529
455
|
# Just add HTML as a fallback, as it will be the most comon one.
|
@@ -607,8 +533,6 @@ class LacusCore():
|
|
607
533
|
browser_engine = 'webkit'
|
608
534
|
try:
|
609
535
|
logger.debug(f'Capturing {url}')
|
610
|
-
# NOTE: starting with python 3.11, we can use asyncio.timeout
|
611
|
-
# async with asyncio.timeout(self.max_capture_time):
|
612
536
|
general_timeout = to_capture.get('general_timeout_in_sec')
|
613
537
|
stats_pipeline.sadd(f'stats:{today}:captures', url)
|
614
538
|
async with Capture(
|
@@ -629,12 +553,19 @@ class LacusCore():
|
|
629
553
|
capture.locale = to_capture.get('locale') # type: ignore[assignment]
|
630
554
|
capture.color_scheme = to_capture.get('color_scheme') # type: ignore[assignment]
|
631
555
|
try:
|
632
|
-
|
556
|
+
# make sure the initialization doesn't take too long
|
557
|
+
if general_timeout is None:
|
558
|
+
general_timeout = 5
|
559
|
+
init_timeout = max(general_timeout / 2, 5)
|
560
|
+
async with timeout(init_timeout) as initialize_timeout:
|
633
561
|
await capture.initialize_context()
|
562
|
+
|
634
563
|
except (TimeoutError, asyncio.exceptions.TimeoutError):
|
564
|
+
timeout_expired(initialize_timeout, logger, 'Initializing took too long.')
|
635
565
|
logger.warning(f'Initializing the context for {url} took longer than the allowed general timeout ({general_timeout}s)')
|
636
566
|
raise RetryCapture(f'Initializing the context for {url} took longer than the allowed general timeout ({general_timeout}s)')
|
637
|
-
|
567
|
+
|
568
|
+
async with timeout(self.max_capture_time) as capture_timeout:
|
638
569
|
playwright_result = await capture.capture_page(
|
639
570
|
url, referer=to_capture.get('referer'),
|
640
571
|
depth=to_capture.get('depth', 0),
|
@@ -660,6 +591,7 @@ class LacusCore():
|
|
660
591
|
# We can give it another short.
|
661
592
|
raise RetryCapture(f'The capture of {url} has been cancelled.')
|
662
593
|
except (TimeoutError, asyncio.exceptions.TimeoutError):
|
594
|
+
timeout_expired(capture_timeout, logger, 'Capture took too long.')
|
663
595
|
logger.warning(f'The capture of {url} took longer than the allowed max capture time ({self.max_capture_time}s)')
|
664
596
|
result = {'error': f'The capture of {url} took longer than the allowed max capture time ({self.max_capture_time}s)'}
|
665
597
|
raise CaptureError(f'The capture of {url} took longer than the allowed max capture time ({self.max_capture_time}s)')
|
@@ -688,7 +620,7 @@ class LacusCore():
|
|
688
620
|
else:
|
689
621
|
current_retry = int(_current_retry.decode())
|
690
622
|
if current_retry > 0:
|
691
|
-
logger.debug(f'Retrying {url} for the {self.max_retries-current_retry+1}th time.')
|
623
|
+
logger.debug(f'Retrying {url} for the {self.max_retries - current_retry + 1}th time.')
|
692
624
|
self.redis.decr(f'lacus:capture_retry:{uuid}')
|
693
625
|
retry = True
|
694
626
|
else:
|
lacuscore/task_logger.py
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Any, Coroutine, Optional, TypeVar, Tuple
|
6
|
+
|
7
|
+
import asyncio
|
8
|
+
import functools
|
9
|
+
import logging
|
10
|
+
|
11
|
+
from .helpers import LacusCoreLogAdapter
|
12
|
+
|
13
|
+
T = TypeVar('T')
|
14
|
+
|
15
|
+
# Code from https://quantlane.com/blog/ensure-asyncio-task-exceptions-get-logged/
|
16
|
+
|
17
|
+
|
18
|
+
def create_task(
|
19
|
+
coroutine: Coroutine[Any, Any, T],
|
20
|
+
*,
|
21
|
+
name: str,
|
22
|
+
logger: 'LacusCoreLogAdapter',
|
23
|
+
message: str,
|
24
|
+
message_args: Tuple[Any, ...] = (),
|
25
|
+
loop: Optional[asyncio.AbstractEventLoop] = None,
|
26
|
+
|
27
|
+
) -> 'asyncio.Task[T]': # This type annotation has to be quoted for Python < 3.9, see https://www.python.org/dev/peps/pep-0585/
|
28
|
+
'''
|
29
|
+
This helper function wraps a ``loop.create_task(coroutine())`` call and ensures there is
|
30
|
+
an exception handler added to the resulting task. If the task raises an exception it is logged
|
31
|
+
using the provided ``logger``, with additional context provided by ``message`` and optionally
|
32
|
+
``message_args``.
|
33
|
+
'''
|
34
|
+
if loop is None:
|
35
|
+
loop = asyncio.get_running_loop()
|
36
|
+
task = loop.create_task(coroutine, name=name)
|
37
|
+
task.add_done_callback(
|
38
|
+
functools.partial(_handle_task_result, logger=logger, message=message, message_args=message_args)
|
39
|
+
)
|
40
|
+
return task
|
41
|
+
|
42
|
+
|
43
|
+
def _handle_task_result(
|
44
|
+
task: asyncio.Task[Any],
|
45
|
+
*,
|
46
|
+
logger: logging.Logger,
|
47
|
+
message: str,
|
48
|
+
message_args: Tuple[Any, ...] = (),
|
49
|
+
) -> None:
|
50
|
+
try:
|
51
|
+
task.result()
|
52
|
+
except asyncio.CancelledError:
|
53
|
+
pass # Task cancellation should not be logged as an error.
|
54
|
+
except asyncio.TimeoutError:
|
55
|
+
pass # Timeout is also fine
|
56
|
+
# Ad the pylint ignore: we want to handle all exceptions here so that the result of the task
|
57
|
+
# is properly logged. There is no point re-raising the exception in this callback.
|
58
|
+
except Exception: # pylint: disable=broad-except
|
59
|
+
logger.exception(message, *message_args)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lacuscore
|
3
|
-
Version: 1.9.
|
3
|
+
Version: 1.9.4
|
4
4
|
Summary: Core of Lacus, usable as a module
|
5
5
|
Home-page: https://github.com/ail-project/LacusCore
|
6
6
|
License: BSD-3-Clause
|
@@ -28,9 +28,9 @@ Requires-Dist: Sphinx (>=7.2,<8.0) ; (python_version >= "3.9") and (extra == "do
|
|
28
28
|
Requires-Dist: async-timeout (>=4.0.3,<5.0.0) ; python_version < "3.11"
|
29
29
|
Requires-Dist: defang (>=0.5.3,<0.6.0)
|
30
30
|
Requires-Dist: dnspython (>=2.6.1,<3.0.0)
|
31
|
-
Requires-Dist: playwrightcapture[recaptcha] (>=1.24.
|
32
|
-
Requires-Dist: redis[hiredis] (>=5.0.
|
33
|
-
Requires-Dist: requests (>=2.
|
31
|
+
Requires-Dist: playwrightcapture[recaptcha] (>=1.24.10,<2.0.0)
|
32
|
+
Requires-Dist: redis[hiredis] (>=5.0.4,<6.0.0)
|
33
|
+
Requires-Dist: requests (>=2.32.1,<3.0.0)
|
34
34
|
Requires-Dist: ua-parser (>=0.18.0,<0.19.0)
|
35
35
|
Project-URL: Documentation, https://lacuscore.readthedocs.io/en/latest/
|
36
36
|
Project-URL: Repository, https://github.com/ail-project/LacusCore
|
@@ -0,0 +1,10 @@
|
|
1
|
+
lacuscore/__init__.py,sha256=hM4lKoPNybDCUMWdXTVVI1gRk_riLvRZ7IwFbamZLzE,341
|
2
|
+
lacuscore/helpers.py,sha256=lULN7HhY-4a4HG-ybIt4jO3wEGTxkm_jKNqsGpNZo4Y,2711
|
3
|
+
lacuscore/lacus_monitoring.py,sha256=UOfE_1-_rhVeKJXQ_m9XxYkr7VwyQnA6iK-x_tcXJfo,2775
|
4
|
+
lacuscore/lacuscore.py,sha256=zi6oOcwmZ0yENIfJy55baeGXw3Js4lg5fmQz0YFRNJk,43030
|
5
|
+
lacuscore/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
lacuscore/task_logger.py,sha256=8WbdJdKnGeFCxt9gtCNLI9vAQQZbsy2I5PRQpHP7XFU,1916
|
7
|
+
lacuscore-1.9.4.dist-info/LICENSE,sha256=4C4hLYrIkUD96Ggk-y_Go1Qf7PBZrEm9PSeTGe2nd4s,1516
|
8
|
+
lacuscore-1.9.4.dist-info/METADATA,sha256=Lr1wGWp4zfC19UwS51sjTxznPywLBZ7-0e9rAF88cYs,2629
|
9
|
+
lacuscore-1.9.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
10
|
+
lacuscore-1.9.4.dist-info/RECORD,,
|
lacuscore-1.9.2.dist-info/RECORD
DELETED
@@ -1,8 +0,0 @@
|
|
1
|
-
lacuscore/__init__.py,sha256=10JsUmjLD9xQIsjTDYT5nGowsJVe9C-FHNkaUFuAUtU,321
|
2
|
-
lacuscore/lacus_monitoring.py,sha256=UOfE_1-_rhVeKJXQ_m9XxYkr7VwyQnA6iK-x_tcXJfo,2775
|
3
|
-
lacuscore/lacuscore.py,sha256=2af-b9vy54FSlcY_YbfVcxsnU1AaeOjGi4UBaeumizs,44432
|
4
|
-
lacuscore/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
lacuscore-1.9.2.dist-info/LICENSE,sha256=4C4hLYrIkUD96Ggk-y_Go1Qf7PBZrEm9PSeTGe2nd4s,1516
|
6
|
-
lacuscore-1.9.2.dist-info/METADATA,sha256=3wrhwQf3_NnZ7FEZZjluPoUaK8d0FEzktfgzgpXi6-0,2628
|
7
|
-
lacuscore-1.9.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
8
|
-
lacuscore-1.9.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|