pybiolib 0.2.951__py3-none-any.whl → 1.2.1890__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.
- biolib/__init__.py +357 -11
- biolib/_data_record/data_record.py +380 -0
- biolib/_index/__init__.py +0 -0
- biolib/_index/index.py +55 -0
- biolib/_index/query_result.py +103 -0
- biolib/_internal/__init__.py +0 -0
- biolib/_internal/add_copilot_prompts.py +58 -0
- biolib/_internal/add_gui_files.py +81 -0
- biolib/_internal/data_record/__init__.py +1 -0
- biolib/_internal/data_record/data_record.py +85 -0
- biolib/_internal/data_record/push_data.py +116 -0
- biolib/_internal/data_record/remote_storage_endpoint.py +43 -0
- biolib/_internal/errors.py +5 -0
- biolib/_internal/file_utils.py +125 -0
- biolib/_internal/fuse_mount/__init__.py +1 -0
- biolib/_internal/fuse_mount/experiment_fuse_mount.py +209 -0
- biolib/_internal/http_client.py +159 -0
- biolib/_internal/lfs/__init__.py +1 -0
- biolib/_internal/lfs/cache.py +51 -0
- biolib/_internal/libs/__init__.py +1 -0
- biolib/_internal/libs/fusepy/__init__.py +1257 -0
- biolib/_internal/push_application.py +488 -0
- biolib/_internal/runtime.py +22 -0
- biolib/_internal/string_utils.py +13 -0
- biolib/_internal/templates/__init__.py +1 -0
- biolib/_internal/templates/copilot_template/.github/instructions/general-app-knowledge.instructions.md +10 -0
- biolib/_internal/templates/copilot_template/.github/instructions/style-general.instructions.md +20 -0
- biolib/_internal/templates/copilot_template/.github/instructions/style-python.instructions.md +16 -0
- biolib/_internal/templates/copilot_template/.github/instructions/style-react-ts.instructions.md +47 -0
- biolib/_internal/templates/copilot_template/.github/prompts/biolib_app_inputs.prompt.md +11 -0
- biolib/_internal/templates/copilot_template/.github/prompts/biolib_onboard_repo.prompt.md +19 -0
- biolib/_internal/templates/copilot_template/.github/prompts/biolib_run_apps.prompt.md +12 -0
- biolib/_internal/templates/dashboard_template/.biolib/config.yml +5 -0
- biolib/_internal/templates/github_workflow_template/.github/workflows/biolib.yml +21 -0
- biolib/_internal/templates/gitignore_template/.gitignore +10 -0
- biolib/_internal/templates/gui_template/.yarnrc.yml +1 -0
- biolib/_internal/templates/gui_template/App.tsx +53 -0
- biolib/_internal/templates/gui_template/Dockerfile +27 -0
- biolib/_internal/templates/gui_template/biolib-sdk.ts +82 -0
- biolib/_internal/templates/gui_template/dev-data/output.json +7 -0
- biolib/_internal/templates/gui_template/index.css +5 -0
- biolib/_internal/templates/gui_template/index.html +13 -0
- biolib/_internal/templates/gui_template/index.tsx +10 -0
- biolib/_internal/templates/gui_template/package.json +27 -0
- biolib/_internal/templates/gui_template/tsconfig.json +24 -0
- biolib/_internal/templates/gui_template/vite-plugin-dev-data.ts +50 -0
- biolib/_internal/templates/gui_template/vite.config.mts +10 -0
- biolib/_internal/templates/init_template/.biolib/config.yml +19 -0
- biolib/_internal/templates/init_template/Dockerfile +14 -0
- biolib/_internal/templates/init_template/requirements.txt +1 -0
- biolib/_internal/templates/init_template/run.py +12 -0
- biolib/_internal/templates/init_template/run.sh +4 -0
- biolib/_internal/templates/templates.py +25 -0
- biolib/_internal/tree_utils.py +106 -0
- biolib/_internal/utils/__init__.py +65 -0
- biolib/_internal/utils/auth.py +46 -0
- biolib/_internal/utils/job_url.py +33 -0
- biolib/_internal/utils/multinode.py +263 -0
- biolib/_runtime/runtime.py +157 -0
- biolib/_session/session.py +44 -0
- biolib/_shared/__init__.py +0 -0
- biolib/_shared/types/__init__.py +74 -0
- biolib/_shared/types/account.py +12 -0
- biolib/_shared/types/account_member.py +8 -0
- biolib/_shared/types/app.py +9 -0
- biolib/_shared/types/data_record.py +40 -0
- biolib/_shared/types/experiment.py +32 -0
- biolib/_shared/types/file_node.py +17 -0
- biolib/_shared/types/push.py +6 -0
- biolib/_shared/types/resource.py +37 -0
- biolib/_shared/types/resource_deploy_key.py +11 -0
- biolib/_shared/types/resource_permission.py +14 -0
- biolib/_shared/types/resource_version.py +19 -0
- biolib/_shared/types/result.py +14 -0
- biolib/_shared/types/typing.py +10 -0
- biolib/_shared/types/user.py +19 -0
- biolib/_shared/utils/__init__.py +7 -0
- biolib/_shared/utils/resource_uri.py +75 -0
- biolib/api/__init__.py +6 -0
- biolib/api/client.py +168 -0
- biolib/app/app.py +252 -49
- biolib/app/search_apps.py +45 -0
- biolib/biolib_api_client/api_client.py +126 -31
- biolib/biolib_api_client/app_types.py +24 -4
- biolib/biolib_api_client/auth.py +31 -8
- biolib/biolib_api_client/biolib_app_api.py +147 -52
- biolib/biolib_api_client/biolib_job_api.py +161 -141
- biolib/biolib_api_client/job_types.py +21 -5
- biolib/biolib_api_client/lfs_types.py +7 -23
- biolib/biolib_api_client/user_state.py +56 -0
- biolib/biolib_binary_format/__init__.py +1 -4
- biolib/biolib_binary_format/file_in_container.py +105 -0
- biolib/biolib_binary_format/module_input.py +24 -7
- biolib/biolib_binary_format/module_output_v2.py +149 -0
- biolib/biolib_binary_format/remote_endpoints.py +34 -0
- biolib/biolib_binary_format/remote_stream_seeker.py +59 -0
- biolib/biolib_binary_format/saved_job.py +3 -2
- biolib/biolib_binary_format/{attestation_document.py → stdout_and_stderr.py} +8 -8
- biolib/biolib_binary_format/system_status_update.py +3 -2
- biolib/biolib_binary_format/utils.py +175 -0
- biolib/biolib_docker_client/__init__.py +11 -2
- biolib/biolib_errors.py +36 -0
- biolib/biolib_logging.py +27 -10
- biolib/cli/__init__.py +38 -0
- biolib/cli/auth.py +46 -0
- biolib/cli/data_record.py +164 -0
- biolib/cli/index.py +32 -0
- biolib/cli/init.py +421 -0
- biolib/cli/lfs.py +101 -0
- biolib/cli/push.py +50 -0
- biolib/cli/run.py +63 -0
- biolib/cli/runtime.py +14 -0
- biolib/cli/sdk.py +16 -0
- biolib/cli/start.py +56 -0
- biolib/compute_node/cloud_utils/cloud_utils.py +110 -161
- biolib/compute_node/job_worker/cache_state.py +66 -88
- biolib/compute_node/job_worker/cache_types.py +1 -6
- biolib/compute_node/job_worker/docker_image_cache.py +112 -37
- biolib/compute_node/job_worker/executors/__init__.py +0 -3
- biolib/compute_node/job_worker/executors/docker_executor.py +532 -199
- biolib/compute_node/job_worker/executors/docker_types.py +9 -1
- biolib/compute_node/job_worker/executors/types.py +19 -9
- biolib/compute_node/job_worker/job_legacy_input_wait_timeout_thread.py +30 -0
- biolib/compute_node/job_worker/job_max_runtime_timer_thread.py +3 -5
- biolib/compute_node/job_worker/job_storage.py +108 -0
- biolib/compute_node/job_worker/job_worker.py +397 -212
- biolib/compute_node/job_worker/large_file_system.py +87 -38
- biolib/compute_node/job_worker/network_alloc.py +99 -0
- biolib/compute_node/job_worker/network_buffer.py +240 -0
- biolib/compute_node/job_worker/utilization_reporter_thread.py +197 -0
- biolib/compute_node/job_worker/utils.py +9 -24
- biolib/compute_node/remote_host_proxy.py +400 -98
- biolib/compute_node/utils.py +31 -9
- biolib/compute_node/webserver/compute_node_results_proxy.py +189 -0
- biolib/compute_node/webserver/proxy_utils.py +28 -0
- biolib/compute_node/webserver/webserver.py +130 -44
- biolib/compute_node/webserver/webserver_types.py +2 -6
- biolib/compute_node/webserver/webserver_utils.py +77 -12
- biolib/compute_node/webserver/worker_thread.py +183 -42
- biolib/experiments/__init__.py +0 -0
- biolib/experiments/experiment.py +356 -0
- biolib/jobs/__init__.py +1 -0
- biolib/jobs/job.py +741 -0
- biolib/jobs/job_result.py +185 -0
- biolib/jobs/types.py +50 -0
- biolib/py.typed +0 -0
- biolib/runtime/__init__.py +14 -0
- biolib/sdk/__init__.py +91 -0
- biolib/tables.py +34 -0
- biolib/typing_utils.py +2 -7
- biolib/user/__init__.py +1 -0
- biolib/user/sign_in.py +54 -0
- biolib/utils/__init__.py +162 -0
- biolib/utils/cache_state.py +94 -0
- biolib/utils/multipart_uploader.py +194 -0
- biolib/utils/seq_util.py +150 -0
- biolib/utils/zip/remote_zip.py +640 -0
- pybiolib-1.2.1890.dist-info/METADATA +41 -0
- pybiolib-1.2.1890.dist-info/RECORD +177 -0
- {pybiolib-0.2.951.dist-info → pybiolib-1.2.1890.dist-info}/WHEEL +1 -1
- pybiolib-1.2.1890.dist-info/entry_points.txt +2 -0
- README.md +0 -17
- biolib/app/app_result.py +0 -68
- biolib/app/utils.py +0 -62
- biolib/biolib-js/0-biolib.worker.js +0 -1
- biolib/biolib-js/1-biolib.worker.js +0 -1
- biolib/biolib-js/2-biolib.worker.js +0 -1
- biolib/biolib-js/3-biolib.worker.js +0 -1
- biolib/biolib-js/4-biolib.worker.js +0 -1
- biolib/biolib-js/5-biolib.worker.js +0 -1
- biolib/biolib-js/6-biolib.worker.js +0 -1
- biolib/biolib-js/index.html +0 -10
- biolib/biolib-js/main-biolib.js +0 -1
- biolib/biolib_api_client/biolib_account_api.py +0 -21
- biolib/biolib_api_client/biolib_large_file_system_api.py +0 -108
- biolib/biolib_binary_format/aes_encrypted_package.py +0 -42
- biolib/biolib_binary_format/module_output.py +0 -58
- biolib/biolib_binary_format/rsa_encrypted_aes_package.py +0 -57
- biolib/biolib_push.py +0 -114
- biolib/cli.py +0 -203
- biolib/cli_utils.py +0 -273
- biolib/compute_node/cloud_utils/enclave_parent_types.py +0 -7
- biolib/compute_node/enclave/__init__.py +0 -2
- biolib/compute_node/enclave/enclave_remote_hosts.py +0 -53
- biolib/compute_node/enclave/nitro_secure_module_utils.py +0 -64
- biolib/compute_node/job_worker/executors/base_executor.py +0 -18
- biolib/compute_node/job_worker/executors/pyppeteer_executor.py +0 -173
- biolib/compute_node/job_worker/executors/remote/__init__.py +0 -1
- biolib/compute_node/job_worker/executors/remote/nitro_enclave_utils.py +0 -81
- biolib/compute_node/job_worker/executors/remote/remote_executor.py +0 -51
- biolib/lfs.py +0 -196
- biolib/pyppeteer/.circleci/config.yml +0 -100
- biolib/pyppeteer/.coveragerc +0 -3
- biolib/pyppeteer/.gitignore +0 -89
- biolib/pyppeteer/.pre-commit-config.yaml +0 -28
- biolib/pyppeteer/CHANGES.md +0 -253
- biolib/pyppeteer/CONTRIBUTING.md +0 -26
- biolib/pyppeteer/LICENSE +0 -12
- biolib/pyppeteer/README.md +0 -137
- biolib/pyppeteer/docs/Makefile +0 -177
- biolib/pyppeteer/docs/_static/custom.css +0 -28
- biolib/pyppeteer/docs/_templates/layout.html +0 -10
- biolib/pyppeteer/docs/changes.md +0 -1
- biolib/pyppeteer/docs/conf.py +0 -299
- biolib/pyppeteer/docs/index.md +0 -21
- biolib/pyppeteer/docs/make.bat +0 -242
- biolib/pyppeteer/docs/reference.md +0 -211
- biolib/pyppeteer/docs/server.py +0 -60
- biolib/pyppeteer/poetry.lock +0 -1699
- biolib/pyppeteer/pyppeteer/__init__.py +0 -135
- biolib/pyppeteer/pyppeteer/accessibility.py +0 -286
- biolib/pyppeteer/pyppeteer/browser.py +0 -401
- biolib/pyppeteer/pyppeteer/browser_fetcher.py +0 -194
- biolib/pyppeteer/pyppeteer/command.py +0 -22
- biolib/pyppeteer/pyppeteer/connection/__init__.py +0 -242
- biolib/pyppeteer/pyppeteer/connection/cdpsession.py +0 -101
- biolib/pyppeteer/pyppeteer/coverage.py +0 -346
- biolib/pyppeteer/pyppeteer/device_descriptors.py +0 -787
- biolib/pyppeteer/pyppeteer/dialog.py +0 -79
- biolib/pyppeteer/pyppeteer/domworld.py +0 -597
- biolib/pyppeteer/pyppeteer/emulation_manager.py +0 -53
- biolib/pyppeteer/pyppeteer/errors.py +0 -48
- biolib/pyppeteer/pyppeteer/events.py +0 -63
- biolib/pyppeteer/pyppeteer/execution_context.py +0 -156
- biolib/pyppeteer/pyppeteer/frame/__init__.py +0 -299
- biolib/pyppeteer/pyppeteer/frame/frame_manager.py +0 -306
- biolib/pyppeteer/pyppeteer/helpers.py +0 -245
- biolib/pyppeteer/pyppeteer/input.py +0 -371
- biolib/pyppeteer/pyppeteer/jshandle.py +0 -598
- biolib/pyppeteer/pyppeteer/launcher.py +0 -683
- biolib/pyppeteer/pyppeteer/lifecycle_watcher.py +0 -169
- biolib/pyppeteer/pyppeteer/models/__init__.py +0 -103
- biolib/pyppeteer/pyppeteer/models/_protocol.py +0 -12460
- biolib/pyppeteer/pyppeteer/multimap.py +0 -82
- biolib/pyppeteer/pyppeteer/network_manager.py +0 -678
- biolib/pyppeteer/pyppeteer/options.py +0 -8
- biolib/pyppeteer/pyppeteer/page.py +0 -1728
- biolib/pyppeteer/pyppeteer/pipe_transport.py +0 -59
- biolib/pyppeteer/pyppeteer/target.py +0 -147
- biolib/pyppeteer/pyppeteer/task_queue.py +0 -24
- biolib/pyppeteer/pyppeteer/timeout_settings.py +0 -36
- biolib/pyppeteer/pyppeteer/tracing.py +0 -93
- biolib/pyppeteer/pyppeteer/us_keyboard_layout.py +0 -305
- biolib/pyppeteer/pyppeteer/util.py +0 -18
- biolib/pyppeteer/pyppeteer/websocket_transport.py +0 -47
- biolib/pyppeteer/pyppeteer/worker.py +0 -101
- biolib/pyppeteer/pyproject.toml +0 -97
- biolib/pyppeteer/spell.txt +0 -137
- biolib/pyppeteer/tox.ini +0 -72
- biolib/pyppeteer/utils/generate_protocol_types.py +0 -603
- biolib/start_cli.py +0 -7
- biolib/utils.py +0 -47
- biolib/validators/validate_app_version.py +0 -183
- biolib/validators/validate_argument.py +0 -134
- biolib/validators/validate_module.py +0 -323
- biolib/validators/validate_zip_file.py +0 -40
- biolib/validators/validator_utils.py +0 -103
- pybiolib-0.2.951.dist-info/LICENSE +0 -21
- pybiolib-0.2.951.dist-info/METADATA +0 -61
- pybiolib-0.2.951.dist-info/RECORD +0 -153
- pybiolib-0.2.951.dist-info/entry_points.txt +0 -3
- /LICENSE → /pybiolib-1.2.1890.dist-info/licenses/LICENSE +0 -0
|
@@ -1,598 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import copy
|
|
3
|
-
import logging
|
|
4
|
-
import math
|
|
5
|
-
import os
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
|
8
|
-
|
|
9
|
-
from biolib.pyppeteer.pyppeteer import helpers
|
|
10
|
-
from biolib.pyppeteer.pyppeteer.connection import CDPSession
|
|
11
|
-
from biolib.pyppeteer.pyppeteer.errors import BrowserError, ElementHandleError, NetworkError
|
|
12
|
-
from biolib.pyppeteer.pyppeteer.models import JSFunctionArg, MouseButton, Protocol
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from pyppeteer.page import Page
|
|
16
|
-
from pyppeteer.frame import FrameManager
|
|
17
|
-
from pyppeteer.execution_context import ExecutionContext
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger(__name__)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def createJSHandle(context, remoteObject) -> Union['JSHandle', 'ElementHandle']:
|
|
24
|
-
frame = context.frame
|
|
25
|
-
if remoteObject.get('subtype') == 'node' and frame:
|
|
26
|
-
frameManager = frame._frameManager
|
|
27
|
-
return ElementHandle(context, context._client, remoteObject, frameManager.page, frameManager)
|
|
28
|
-
return JSHandle(context, context._client, remoteObject,)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class JSHandle:
|
|
32
|
-
"""JSHandle class.
|
|
33
|
-
|
|
34
|
-
JSHandle represents an in-page JavaScript object. JSHandle can be created
|
|
35
|
-
with the :meth:`~pyppeteer.page.Page.evaluateHandle` method.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
def __init__(self, context: 'ExecutionContext', client: 'CDPSession', remoteObject: Protocol.Runtime.RemoteObject):
|
|
39
|
-
self._context = context
|
|
40
|
-
self._client = client
|
|
41
|
-
self._remoteObject = remoteObject
|
|
42
|
-
self._disposed = False
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def executionContext(self):
|
|
46
|
-
"""Get execution context of this handle."""
|
|
47
|
-
return self._context
|
|
48
|
-
|
|
49
|
-
async def evaluate(self, pageFunction: str, *args: JSFunctionArg):
|
|
50
|
-
return await self.executionContext.evaluate(pageFunction, self, *args)
|
|
51
|
-
|
|
52
|
-
async def evaluateHandle(self, pageFunction: str, *args: JSFunctionArg):
|
|
53
|
-
return await self.executionContext.evaluateHandle(pageFunction, self, *args)
|
|
54
|
-
|
|
55
|
-
async def getProperty(self, propertyName: str) -> 'JSHandle':
|
|
56
|
-
"""Get property value of ``propertyName``."""
|
|
57
|
-
objectHandle = await self._context.evaluateHandle(
|
|
58
|
-
'''(object, propertyName) => {
|
|
59
|
-
const result = {__proto__: null};
|
|
60
|
-
result[propertyName] = object[propertyName];
|
|
61
|
-
return result;
|
|
62
|
-
}''',
|
|
63
|
-
self,
|
|
64
|
-
propertyName,
|
|
65
|
-
)
|
|
66
|
-
properties = await objectHandle.getProperties()
|
|
67
|
-
result = properties[propertyName]
|
|
68
|
-
await objectHandle.dispose()
|
|
69
|
-
return result
|
|
70
|
-
|
|
71
|
-
async def getProperties(self) -> Dict[str, 'JSHandle']:
|
|
72
|
-
"""Get all properties of this handle."""
|
|
73
|
-
response = await self._client.send(
|
|
74
|
-
'Runtime.getProperties', {'objectId': self._remoteObject.get('objectId', ''), 'ownProperties': True,}
|
|
75
|
-
)
|
|
76
|
-
result = {}
|
|
77
|
-
for prop in response['result']:
|
|
78
|
-
if not prop.get('enumerable'):
|
|
79
|
-
continue
|
|
80
|
-
result[prop['name']] = createJSHandle(self._context, prop['value'])
|
|
81
|
-
return result
|
|
82
|
-
|
|
83
|
-
async def jsonValue(self) -> Dict:
|
|
84
|
-
"""Get Jsonized value of this object."""
|
|
85
|
-
objectId = self._remoteObject.get('objectId')
|
|
86
|
-
if objectId:
|
|
87
|
-
response = await self._client.send(
|
|
88
|
-
'Runtime.callFunctionOn',
|
|
89
|
-
{
|
|
90
|
-
'functionDeclaration': 'function() { return this; }',
|
|
91
|
-
'objectId': objectId,
|
|
92
|
-
'returnByValue': True,
|
|
93
|
-
'awaitPromise': True,
|
|
94
|
-
},
|
|
95
|
-
)
|
|
96
|
-
return helpers.valueFromRemoteObject(response['result'])
|
|
97
|
-
return helpers.valueFromRemoteObject(self._remoteObject)
|
|
98
|
-
|
|
99
|
-
def asElement(self) -> 'ElementHandle':
|
|
100
|
-
...
|
|
101
|
-
|
|
102
|
-
async def dispose(self) -> None:
|
|
103
|
-
"""Stop referencing the handle."""
|
|
104
|
-
if self._disposed:
|
|
105
|
-
return
|
|
106
|
-
self._disposed = True
|
|
107
|
-
await helpers.releaseObject(self._client, self._remoteObject)
|
|
108
|
-
|
|
109
|
-
def toString(self) -> str:
|
|
110
|
-
"""Get string representation."""
|
|
111
|
-
if self._remoteObject.get('objectId'):
|
|
112
|
-
_type = self._remoteObject.get('subtype') or self._remoteObject.get('type')
|
|
113
|
-
return f'JSHandle@{_type}'
|
|
114
|
-
return 'JSHandle:{}'.format(helpers.valueFromRemoteObject(self._remoteObject))
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
class ElementHandle(JSHandle):
|
|
118
|
-
def __init__(
|
|
119
|
-
self,
|
|
120
|
-
context: 'ExecutionContext',
|
|
121
|
-
client: CDPSession,
|
|
122
|
-
remoteObject: Protocol.Runtime.RemoteObject,
|
|
123
|
-
page: 'Page',
|
|
124
|
-
frameManager: 'FrameManager',
|
|
125
|
-
):
|
|
126
|
-
super().__init__(context, client, remoteObject)
|
|
127
|
-
self._page = page
|
|
128
|
-
self._frameManager = frameManager
|
|
129
|
-
self._disposed = False
|
|
130
|
-
|
|
131
|
-
# Aliases for query methods:
|
|
132
|
-
self.J = self.querySelector
|
|
133
|
-
self.Jx = self.xpath
|
|
134
|
-
self.Jeval = self.querySelectorEval
|
|
135
|
-
self.JJ = self.querySelectorAll
|
|
136
|
-
self.JJeval = self.querySelectorAllEval
|
|
137
|
-
|
|
138
|
-
def asElement(self) -> Optional['ElementHandle']:
|
|
139
|
-
return self
|
|
140
|
-
|
|
141
|
-
async def contentFrame(self):
|
|
142
|
-
nodeInfo = await self._client.send('DOM.describeNode', {'objectId': self._remoteObject.get('objectId')})
|
|
143
|
-
frameId = nodeInfo.get('node', {}).get('frameId')
|
|
144
|
-
if isinstance(frameId, str):
|
|
145
|
-
return self._frameManager.frame(frameId)
|
|
146
|
-
|
|
147
|
-
async def _scrollIntoViewIfNeeded(self):
|
|
148
|
-
error = await self.evaluate(
|
|
149
|
-
"""
|
|
150
|
-
async(element, pageJavascriptEnabled) => {
|
|
151
|
-
if (!element.isConnected)
|
|
152
|
-
return 'Node is detached from document';
|
|
153
|
-
if (element.nodeType !== Node.ELEMENT_NODE)
|
|
154
|
-
return 'Node is not of type HTMLElement';
|
|
155
|
-
// force-scroll if page's javascript is disabled.
|
|
156
|
-
if (!pageJavascriptEnabled) {
|
|
157
|
-
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
const visibleRatio = await new Promise(resolve => {
|
|
161
|
-
const observer = new IntersectionObserver(entries => {
|
|
162
|
-
resolve(entries[0].intersectionRatio);
|
|
163
|
-
observer.disconnect();
|
|
164
|
-
});
|
|
165
|
-
observer.observe(element);
|
|
166
|
-
});
|
|
167
|
-
if (visibleRatio !== 1.0)
|
|
168
|
-
element.scrollIntoView({block: 'center', inline: 'center', behavior: 'instant'});
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
""",
|
|
172
|
-
self._page._javascriptEnabled,
|
|
173
|
-
)
|
|
174
|
-
if error:
|
|
175
|
-
raise BrowserError(error)
|
|
176
|
-
|
|
177
|
-
async def _clickablePoint(self):
|
|
178
|
-
# swallow any errors to get a better error message
|
|
179
|
-
# before: Protocol error (DOM.getContentQuads): Could not compute content quads.
|
|
180
|
-
# after swallowing: Node is either invisible or not an HTMLElement
|
|
181
|
-
async def silent_get_quads():
|
|
182
|
-
try:
|
|
183
|
-
return await self._client.send('DOM.getContentQuads', {'objectId': self._remoteObject['objectId']})
|
|
184
|
-
except NetworkError:
|
|
185
|
-
logger.exception('Failed to retrieve content quads')
|
|
186
|
-
return None
|
|
187
|
-
|
|
188
|
-
result, layoutMetrics = await asyncio.gather(silent_get_quads(), self._client.send('Page.getLayoutMetrics'),)
|
|
189
|
-
if not result or not result.get('quads'):
|
|
190
|
-
raise BrowserError('Node is either not visible or not an HTMLElement')
|
|
191
|
-
clientWidth = layoutMetrics['layoutViewport']['clientWidth']
|
|
192
|
-
clientHeight = layoutMetrics['layoutViewport']['clientHeight']
|
|
193
|
-
quads = []
|
|
194
|
-
for quad in result['quads']:
|
|
195
|
-
quad = self._intersectQuadWithViewport(self._fromProtocolQuad(quad), clientWidth, clientHeight)
|
|
196
|
-
if computeQuadArea(quad) > 1:
|
|
197
|
-
quads.append(quad)
|
|
198
|
-
if not quads:
|
|
199
|
-
raise BrowserError('Node is either not visible or not an HTMLElement')
|
|
200
|
-
# return middle of quad[0]
|
|
201
|
-
quad = quads[0]
|
|
202
|
-
x, y = 0, 0
|
|
203
|
-
for point in quad:
|
|
204
|
-
x += point['x']
|
|
205
|
-
y += point['y']
|
|
206
|
-
return {
|
|
207
|
-
'x': x / 4,
|
|
208
|
-
'y': y / 4,
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async def _getBoxModel(self):
|
|
212
|
-
try:
|
|
213
|
-
return await self._client.send('DOM.getBoxModel', {'objectId': self._remoteObject['objectId']})
|
|
214
|
-
except Exception as e:
|
|
215
|
-
logger.error(f'An exception occurred: {e}')
|
|
216
|
-
|
|
217
|
-
def _fromProtocolQuad(self, quad):
|
|
218
|
-
return [
|
|
219
|
-
{'x': quad[0], 'y': quad[1]},
|
|
220
|
-
{'x': quad[2], 'y': quad[3]},
|
|
221
|
-
{'x': quad[4], 'y': quad[5]},
|
|
222
|
-
{'x': quad[6], 'y': quad[7]},
|
|
223
|
-
]
|
|
224
|
-
|
|
225
|
-
def _intersectQuadWithViewport(self, quad: List[Dict[str, float]], width: float, height: float):
|
|
226
|
-
return [{'x': min(max(point['x'], 0), width), 'y': min(max(point['y'], 0), height)} for point in quad]
|
|
227
|
-
|
|
228
|
-
async def hover(self) -> None:
|
|
229
|
-
"""Move mouse over to center of this element.
|
|
230
|
-
|
|
231
|
-
If needed, this method scrolls element into view. If this element is
|
|
232
|
-
detached from DOM tree, the method raises an ``ElementHandleError``.
|
|
233
|
-
"""
|
|
234
|
-
await self._scrollIntoViewIfNeeded()
|
|
235
|
-
obj = await self._clickablePoint()
|
|
236
|
-
x = obj.get('x', 0)
|
|
237
|
-
y = obj.get('y', 0)
|
|
238
|
-
await self._page.mouse.move(x, y)
|
|
239
|
-
|
|
240
|
-
async def click(self, button: MouseButton = 'left', clickCount: int = 1, delay: float = 0) -> None:
|
|
241
|
-
"""Click the center of this element.
|
|
242
|
-
|
|
243
|
-
If needed, this method scrolls element into view. If the element is
|
|
244
|
-
detached from DOM, the method raises ``ElementHandleError``.
|
|
245
|
-
|
|
246
|
-
``options`` can contain the following fields:
|
|
247
|
-
|
|
248
|
-
* ``button`` (str): ``left``, ``right``, of ``middle``, defaults to
|
|
249
|
-
``left``.
|
|
250
|
-
* ``clickCount`` (int): Defaults to 1.
|
|
251
|
-
* ``delay`` (int|float): Time to wait between ``mousedown`` and
|
|
252
|
-
``mouseup`` in milliseconds. Defaults to 0.
|
|
253
|
-
"""
|
|
254
|
-
await self._scrollIntoViewIfNeeded()
|
|
255
|
-
point = await self._clickablePoint()
|
|
256
|
-
x = point.get('x', 0)
|
|
257
|
-
y = point.get('y', 0)
|
|
258
|
-
await self._page.mouse.click(x, y, button=button, clickCount=clickCount, delay=delay)
|
|
259
|
-
|
|
260
|
-
async def select(self, *values: str) -> List[str]:
|
|
261
|
-
for val in values:
|
|
262
|
-
if not isinstance(val, str):
|
|
263
|
-
raise ValueError(f'value "{val}" needs to be of type str, but found value of type {type(val)}')
|
|
264
|
-
return await self.evaluate(
|
|
265
|
-
"""(element, values) => {
|
|
266
|
-
if (element.nodeName.toLowerCase() !== 'select')
|
|
267
|
-
throw new Error('Element is not a <select> element.');
|
|
268
|
-
|
|
269
|
-
const options = Array.from(element.options);
|
|
270
|
-
element.value = undefined;
|
|
271
|
-
for (const option of options) {
|
|
272
|
-
option.selected = values.includes(option.value);
|
|
273
|
-
if (option.selected && !element.multiple)
|
|
274
|
-
break;
|
|
275
|
-
}
|
|
276
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
277
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
278
|
-
return options.filter(option => option.selected).map(option => option.value);
|
|
279
|
-
}
|
|
280
|
-
""",
|
|
281
|
-
values,
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
async def uploadFile(self, *filePaths: Union[Path, str]) -> dict:
|
|
285
|
-
"""Upload files."""
|
|
286
|
-
# TODO port this
|
|
287
|
-
files = [os.path.abspath(p) for p in filePaths]
|
|
288
|
-
objectId = self._remoteObject.get('objectId')
|
|
289
|
-
return await self._client.send('DOM.setFileInputFiles', {'objectId': objectId, 'files': files})
|
|
290
|
-
|
|
291
|
-
async def tap(self) -> None:
|
|
292
|
-
"""Tap the center of this element.
|
|
293
|
-
|
|
294
|
-
If needed, this method scrolls element into view. If the element is
|
|
295
|
-
detached from DOM, the method raises ``ElementHandleError``.
|
|
296
|
-
"""
|
|
297
|
-
await self._scrollIntoViewIfNeeded()
|
|
298
|
-
center = await self._clickablePoint()
|
|
299
|
-
x = center.get('x', 0)
|
|
300
|
-
y = center.get('y', 0)
|
|
301
|
-
await self._page.touchscreen.tap(x, y)
|
|
302
|
-
|
|
303
|
-
async def focus(self) -> None:
|
|
304
|
-
"""Focus on this element."""
|
|
305
|
-
await self.executionContext.evaluate('element => element.focus()', self)
|
|
306
|
-
|
|
307
|
-
async def type(self, text: str, delay: float = 0) -> None:
|
|
308
|
-
"""Focus the element and then type text.
|
|
309
|
-
|
|
310
|
-
Details see :meth:`pyppeteer.input.Keyboard.type` method.
|
|
311
|
-
"""
|
|
312
|
-
await self.focus()
|
|
313
|
-
await self._page.keyboard.type(text, delay)
|
|
314
|
-
|
|
315
|
-
async def press(self, key: str, text: str = None, delay: float = 0) -> None:
|
|
316
|
-
"""Press ``key`` onto the element.
|
|
317
|
-
|
|
318
|
-
This method focuses the element, and then uses
|
|
319
|
-
:meth:`pyppeteer.input.keyboard.down` and
|
|
320
|
-
:meth:`pyppeteer.input.keyboard.up`.
|
|
321
|
-
|
|
322
|
-
:arg str key: Name of key to press, such as ``ArrowLeft``.
|
|
323
|
-
|
|
324
|
-
This method accepts the following options:
|
|
325
|
-
|
|
326
|
-
* ``text`` (str): If specified, generates an input event with this
|
|
327
|
-
text.
|
|
328
|
-
* ``delay`` (int|float): Time to wait between ``keydown`` and
|
|
329
|
-
``keyup``. Defaults to 0.
|
|
330
|
-
"""
|
|
331
|
-
await self.focus()
|
|
332
|
-
await self._page.keyboard.press(key, text=text, delay=delay)
|
|
333
|
-
|
|
334
|
-
async def boundingBox(self) -> Optional[Dict[str, float]]:
|
|
335
|
-
"""Return bounding box of this element.
|
|
336
|
-
|
|
337
|
-
If the element is not visible, return ``None``.
|
|
338
|
-
|
|
339
|
-
This method returns dictionary of bounding box, which contains:
|
|
340
|
-
|
|
341
|
-
* ``x`` (int): The X coordinate of the element in pixels.
|
|
342
|
-
* ``y`` (int): The Y coordinate of the element in pixels.
|
|
343
|
-
* ``width`` (int): The width of the element in pixels.
|
|
344
|
-
* ``height`` (int): The height of the element in pixels.
|
|
345
|
-
"""
|
|
346
|
-
result = await self._getBoxModel()
|
|
347
|
-
|
|
348
|
-
if not result:
|
|
349
|
-
return None
|
|
350
|
-
|
|
351
|
-
quad = result['model']['border']
|
|
352
|
-
x = min(quad[0], quad[2], quad[4], quad[6])
|
|
353
|
-
y = min(quad[1], quad[3], quad[5], quad[7])
|
|
354
|
-
width = max(quad[0], quad[2], quad[4], quad[6]) - x
|
|
355
|
-
height = max(quad[1], quad[3], quad[5], quad[7]) - y
|
|
356
|
-
return {'x': x, 'y': y, 'width': width, 'height': height}
|
|
357
|
-
|
|
358
|
-
async def boxModel(self) -> Optional[Dict]:
|
|
359
|
-
"""Return boxes of element.
|
|
360
|
-
|
|
361
|
-
Return ``None`` if element is not visible. Boxes are represented as an
|
|
362
|
-
list of points; each Point is a dictionary ``{x, y}``. Box points are
|
|
363
|
-
sorted clock-wise.
|
|
364
|
-
|
|
365
|
-
Returned value is a dictionary with the following fields:
|
|
366
|
-
|
|
367
|
-
* ``content`` (List[Dict]): Content box.
|
|
368
|
-
* ``padding`` (List[Dict]): Padding box.
|
|
369
|
-
* ``border`` (List[Dict]): Border box.
|
|
370
|
-
* ``margin`` (List[Dict]): Margin box.
|
|
371
|
-
* ``width`` (int): Element's width.
|
|
372
|
-
* ``height`` (int): Element's height.
|
|
373
|
-
"""
|
|
374
|
-
result = await self._getBoxModel()
|
|
375
|
-
|
|
376
|
-
if not result:
|
|
377
|
-
return None
|
|
378
|
-
|
|
379
|
-
model = result.get('model', {})
|
|
380
|
-
return {
|
|
381
|
-
'content': self._fromProtocolQuad(model.get('content')),
|
|
382
|
-
'padding': self._fromProtocolQuad(model.get('padding')),
|
|
383
|
-
'border': self._fromProtocolQuad(model.get('border')),
|
|
384
|
-
'margin': self._fromProtocolQuad(model.get('margin')),
|
|
385
|
-
'width': model.get('width'),
|
|
386
|
-
'height': model.get('height'),
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
async def screenshot(
|
|
390
|
-
self,
|
|
391
|
-
path: Union[str, Path] = None,
|
|
392
|
-
type_: str = 'png', # png or jpeg
|
|
393
|
-
quality: int = None, # 0 to 100
|
|
394
|
-
fullPage: bool = False,
|
|
395
|
-
omitBackground: bool = False,
|
|
396
|
-
encoding: str = 'binary',
|
|
397
|
-
) -> bytes:
|
|
398
|
-
"""Take a screenshot of this element.
|
|
399
|
-
|
|
400
|
-
If the element is detached from DOM, this method raises an
|
|
401
|
-
``ElementHandleError``.
|
|
402
|
-
|
|
403
|
-
Available options are same as :meth:`pyppeteer.page.Page.screenshot`.
|
|
404
|
-
"""
|
|
405
|
-
|
|
406
|
-
needsViewportReset = False
|
|
407
|
-
boundingBox = await self.boundingBox()
|
|
408
|
-
if not boundingBox:
|
|
409
|
-
raise ElementHandleError('Node is either not visible or not an HTMLElement')
|
|
410
|
-
|
|
411
|
-
original_viewport = copy.deepcopy(self._page.viewport)
|
|
412
|
-
|
|
413
|
-
if boundingBox['width'] > original_viewport['width'] or boundingBox['height'] > original_viewport['height']:
|
|
414
|
-
newViewport = {
|
|
415
|
-
'width': max(original_viewport['width'], math.ceil(boundingBox['width'])),
|
|
416
|
-
'height': max(original_viewport['height'], math.ceil(boundingBox['height'])),
|
|
417
|
-
}
|
|
418
|
-
new_viewport = copy.deepcopy(original_viewport)
|
|
419
|
-
new_viewport.update(newViewport)
|
|
420
|
-
await self._page.setViewport(new_viewport)
|
|
421
|
-
needsViewportReset = True
|
|
422
|
-
|
|
423
|
-
await self._scrollIntoViewIfNeeded()
|
|
424
|
-
boundingBox = await self.boundingBox()
|
|
425
|
-
if not boundingBox:
|
|
426
|
-
raise ElementHandleError('Node is either not visible or not an HTMLElement')
|
|
427
|
-
|
|
428
|
-
_obj = await self._client.send('Page.getLayoutMetrics')
|
|
429
|
-
pageX = _obj['layoutViewport']['pageX']
|
|
430
|
-
pageY = _obj['layoutViewport']['pageY']
|
|
431
|
-
|
|
432
|
-
clip = {}
|
|
433
|
-
clip.update(boundingBox)
|
|
434
|
-
clip['x'] = clip['x'] + pageX
|
|
435
|
-
clip['y'] = clip['y'] + pageY
|
|
436
|
-
|
|
437
|
-
imageData = await self._page.screenshot(
|
|
438
|
-
path=path,
|
|
439
|
-
type_=type_,
|
|
440
|
-
quality=quality,
|
|
441
|
-
fullPage=fullPage,
|
|
442
|
-
clip=clip,
|
|
443
|
-
omitBackground=omitBackground,
|
|
444
|
-
encoding=encoding,
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
if needsViewportReset:
|
|
448
|
-
await self._page.setViewport(original_viewport)
|
|
449
|
-
|
|
450
|
-
return imageData
|
|
451
|
-
|
|
452
|
-
async def querySelector(self, selector: str) -> Optional['ElementHandle']:
|
|
453
|
-
"""Return first element which matches ``selector`` under this element.
|
|
454
|
-
|
|
455
|
-
If no element matches the ``selector``, returns ``None``.
|
|
456
|
-
"""
|
|
457
|
-
handle = await self.evaluateHandle('(element, selector) => element.querySelector(selector)', selector,)
|
|
458
|
-
element = handle.asElement()
|
|
459
|
-
if element:
|
|
460
|
-
return element
|
|
461
|
-
await handle.dispose()
|
|
462
|
-
return None
|
|
463
|
-
|
|
464
|
-
async def querySelectorAll(self, selector: str) -> List['ElementHandle']:
|
|
465
|
-
"""Return all elements which match ``selector`` under this element.
|
|
466
|
-
|
|
467
|
-
If no element matches the ``selector``, returns empty list (``[]``).
|
|
468
|
-
"""
|
|
469
|
-
arrayHandle = await self.executionContext.evaluateHandle(
|
|
470
|
-
'(element, selector) => element.querySelectorAll(selector)', self, selector,
|
|
471
|
-
)
|
|
472
|
-
properties = await arrayHandle.getProperties()
|
|
473
|
-
await arrayHandle.dispose()
|
|
474
|
-
result = []
|
|
475
|
-
for prop in properties.values():
|
|
476
|
-
elementHandle = prop.asElement()
|
|
477
|
-
if elementHandle:
|
|
478
|
-
result.append(elementHandle)
|
|
479
|
-
return result # type: ignore
|
|
480
|
-
|
|
481
|
-
async def querySelectorEval(self, selector: str, pageFunction: str, *args: JSFunctionArg) -> Any:
|
|
482
|
-
"""Run ``Page.querySelectorEval`` within the element.
|
|
483
|
-
|
|
484
|
-
This method runs ``document.querySelector`` within the element and
|
|
485
|
-
passes it as the first argument to ``pageFunction``. If there is no
|
|
486
|
-
element matching ``selector``, the method raises
|
|
487
|
-
``ElementHandleError``.
|
|
488
|
-
|
|
489
|
-
If ``pageFunction`` returns a promise, then wait for the promise to
|
|
490
|
-
resolve and return its value.
|
|
491
|
-
|
|
492
|
-
``ElementHandle.Jeval`` is a shortcut of this method.
|
|
493
|
-
|
|
494
|
-
Example:
|
|
495
|
-
|
|
496
|
-
.. code:: python
|
|
497
|
-
|
|
498
|
-
tweetHandle = await page.querySelector('.tweet')
|
|
499
|
-
assert (await tweetHandle.querySelectorEval('.like', 'node => node.innerText')) == 100
|
|
500
|
-
assert (await tweetHandle.Jeval('.retweets', 'node => node.innerText')) == 10
|
|
501
|
-
"""
|
|
502
|
-
elementHandle = await self.querySelector(selector)
|
|
503
|
-
if not elementHandle:
|
|
504
|
-
raise ElementHandleError(f'Error: failed to find element matching selector "{selector}"')
|
|
505
|
-
result = await self.executionContext.evaluate(pageFunction, elementHandle, *args)
|
|
506
|
-
await elementHandle.dispose()
|
|
507
|
-
return result
|
|
508
|
-
|
|
509
|
-
async def querySelectorAllEval(self, selector: str, pageFunction: str, *args: JSFunctionArg) -> Any:
|
|
510
|
-
"""Run ``Page.querySelectorAllEval`` within the element.
|
|
511
|
-
|
|
512
|
-
This method runs ``Array.from(document.querySelectorAll)`` within the
|
|
513
|
-
element and passes it as the first argument to ``pageFunction``. If
|
|
514
|
-
there is no element matching ``selector``, the method raises
|
|
515
|
-
``ElementHandleError``.
|
|
516
|
-
|
|
517
|
-
If ``pageFunction`` returns a promise, then wait for the promise to
|
|
518
|
-
resolve and return its value.
|
|
519
|
-
|
|
520
|
-
Example:
|
|
521
|
-
|
|
522
|
-
.. code:: html
|
|
523
|
-
|
|
524
|
-
<div class="feed">
|
|
525
|
-
<div class="tweet">Hello!</div>
|
|
526
|
-
<div class="tweet">Hi!</div>
|
|
527
|
-
</div>
|
|
528
|
-
|
|
529
|
-
.. code:: python
|
|
530
|
-
|
|
531
|
-
feedHandle = await page.J('.feed')
|
|
532
|
-
assert (await feedHandle.JJeval('.tweet', '(nodes => nodes.map(n => n.innerText))')) == ['Hello!', 'Hi!']
|
|
533
|
-
"""
|
|
534
|
-
arrayHandle = await self.executionContext.evaluateHandle(
|
|
535
|
-
'(element, selector) => Array.from(element.querySelectorAll(selector))', self, selector
|
|
536
|
-
)
|
|
537
|
-
result = await self.executionContext.evaluate(pageFunction, arrayHandle, *args)
|
|
538
|
-
await arrayHandle.dispose()
|
|
539
|
-
return result
|
|
540
|
-
|
|
541
|
-
async def xpath(self, expression: str) -> List['ElementHandle']:
|
|
542
|
-
"""Evaluate the XPath expression relative to this elementHandle.
|
|
543
|
-
|
|
544
|
-
If there are no such elements, return an empty list.
|
|
545
|
-
|
|
546
|
-
:arg str expression: XPath string to be evaluated.
|
|
547
|
-
"""
|
|
548
|
-
arrayHandle = await self.executionContext.evaluateHandle(
|
|
549
|
-
'''(element, expression) => {
|
|
550
|
-
const document = element.ownerDocument || element;
|
|
551
|
-
const iterator = document.evaluate(expression, element, null,
|
|
552
|
-
XPathResult.ORDERED_NODE_ITERATOR_TYPE);
|
|
553
|
-
const array = [];
|
|
554
|
-
let item;
|
|
555
|
-
while ((item = iterator.iterateNext()))
|
|
556
|
-
array.push(item);
|
|
557
|
-
return array;
|
|
558
|
-
|
|
559
|
-
}''',
|
|
560
|
-
self,
|
|
561
|
-
expression,
|
|
562
|
-
)
|
|
563
|
-
properties = await arrayHandle.getProperties()
|
|
564
|
-
await arrayHandle.dispose()
|
|
565
|
-
result = []
|
|
566
|
-
for property in properties.values():
|
|
567
|
-
elementHandle = property.asElement()
|
|
568
|
-
if elementHandle:
|
|
569
|
-
result.append(elementHandle)
|
|
570
|
-
return result
|
|
571
|
-
|
|
572
|
-
#: alias to :meth:`xpath`
|
|
573
|
-
Jx = xpath
|
|
574
|
-
|
|
575
|
-
async def isIntersectingViewport(self) -> bool:
|
|
576
|
-
"""Return ``True`` if the element is visible in the viewport."""
|
|
577
|
-
return await self.executionContext.evaluate(
|
|
578
|
-
'''async element => {
|
|
579
|
-
const visibleRatio = await new Promise(resolve => {
|
|
580
|
-
const observer = new IntersectionObserver(entries => {
|
|
581
|
-
resolve(entries[0].intersectionRatio);
|
|
582
|
-
observer.disconnect();
|
|
583
|
-
});
|
|
584
|
-
observer.observe(element);
|
|
585
|
-
});
|
|
586
|
-
return visibleRatio > 0;
|
|
587
|
-
}''',
|
|
588
|
-
self,
|
|
589
|
-
)
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
def computeQuadArea(quad: List[Dict]) -> float:
|
|
593
|
-
area = 0
|
|
594
|
-
for i in range(len(quad)):
|
|
595
|
-
p1 = quad[i]
|
|
596
|
-
p2 = quad[(i + 1) % len(quad)]
|
|
597
|
-
area += (p1['x'] * p2['y'] - p2['x'] * p1['y']) / 2
|
|
598
|
-
return abs(area)
|