Best Python code snippet using playwright-python
_base.py
Source:_base.py
1"""Commons function for Sync and Async client."""2from __future__ import absolute_import3import base644import codecs5import configparser6import csv7import os8from datetime import datetime, timedelta9from typing import Iterator, List, Generator, Any, Union, Iterable, AsyncGenerator10from urllib3 import HTTPResponse11from influxdb_client import Configuration, Dialect, Query, OptionStatement, VariableAssignment, Identifier, \12 Expression, BooleanLiteral, IntegerLiteral, FloatLiteral, DateTimeLiteral, UnaryExpression, DurationLiteral, \13 Duration, StringLiteral, ArrayExpression, ImportDeclaration, MemberExpression, MemberAssignment, File, \14 WriteService, QueryService, DeleteService, DeletePredicateRequest15from influxdb_client.client.flux_csv_parser import FluxResponseMetadataMode, FluxCsvParser, FluxSerializationMode16from influxdb_client.client.flux_table import FluxTable, FluxRecord17from influxdb_client.client.util.date_utils import get_date_helper18from influxdb_client.client.util.helpers import get_org_query_param19from influxdb_client.client.write.dataframe_serializer import DataframeSerializer20from influxdb_client.rest import _UTF_8_encoding21try:22 import dataclasses23 _HAS_DATACLASS = True24except ModuleNotFoundError:25 _HAS_DATACLASS = False26# noinspection PyMethodMayBeStatic27class _BaseClient(object):28 def __init__(self, url, token, debug=None, timeout=10_000, enable_gzip=False, org: str = None,29 default_tags: dict = None, **kwargs) -> None:30 self.url = url31 self.token = token32 self.org = org33 self.default_tags = default_tags34 self.conf = _Configuration()35 if self.url.endswith("/"):36 self.conf.host = self.url[:-1]37 else:38 self.conf.host = self.url39 self.conf.enable_gzip = enable_gzip40 self.conf.debug = debug41 self.conf.verify_ssl = kwargs.get('verify_ssl', True)42 self.conf.ssl_ca_cert = kwargs.get('ssl_ca_cert', None)43 self.conf.proxy = kwargs.get('proxy', None)44 self.conf.proxy_headers = kwargs.get('proxy_headers', None)45 self.conf.connection_pool_maxsize = kwargs.get('connection_pool_maxsize', self.conf.connection_pool_maxsize)46 self.conf.timeout = timeout47 auth_token = self.token48 self.auth_header_name = "Authorization"49 self.auth_header_value = "Token " + auth_token50 auth_basic = kwargs.get('auth_basic', False)51 if auth_basic:52 self.auth_header_value = "Basic " + base64.b64encode(token.encode()).decode()53 self.retries = kwargs.get('retries', False)54 self.profilers = kwargs.get('profilers', None)55 pass56 def _version(self, response) -> str:57 if response is not None and len(response) >= 3:58 if 'X-Influxdb-Version' in response[2]:59 return response[2]['X-Influxdb-Version']60 return "unknown"61 @classmethod62 def _from_config_file(cls, config_file: str = "config.ini", debug=None, enable_gzip=False):63 config = configparser.ConfigParser()64 config.read(config_file)65 def config_value(key: str):66 return config['influx2'][key].strip('"')67 url = config_value('url')68 token = config_value('token')69 timeout = None70 if config.has_option('influx2', 'timeout'):71 timeout = config_value('timeout')72 org = None73 if config.has_option('influx2', 'org'):74 org = config_value('org')75 verify_ssl = True76 if config.has_option('influx2', 'verify_ssl'):77 verify_ssl = config_value('verify_ssl')78 ssl_ca_cert = None79 if config.has_option('influx2', 'ssl_ca_cert'):80 ssl_ca_cert = config_value('ssl_ca_cert')81 connection_pool_maxsize = None82 if config.has_option('influx2', 'connection_pool_maxsize'):83 connection_pool_maxsize = config_value('connection_pool_maxsize')84 auth_basic = False85 if config.has_option('influx2', 'auth_basic'):86 auth_basic = config_value('auth_basic')87 default_tags = None88 if config.has_section('tags'):89 tags = {k: v.strip('"') for k, v in config.items('tags')}90 default_tags = dict(tags)91 profilers = None92 if config.has_option('influx2', 'profilers'):93 profilers = [x.strip() for x in config_value('profilers').split(',')]94 proxy = None95 if config.has_option('influx2', 'proxy'):96 proxy = config_value('proxy')97 return cls(url, token, debug=debug, timeout=_to_int(timeout), org=org, default_tags=default_tags,98 enable_gzip=enable_gzip, verify_ssl=_to_bool(verify_ssl), ssl_ca_cert=ssl_ca_cert,99 connection_pool_maxsize=_to_int(connection_pool_maxsize), auth_basic=_to_bool(auth_basic),100 profilers=profilers, proxy=proxy)101 @classmethod102 def _from_env_properties(cls, debug=None, enable_gzip=False):103 url = os.getenv('INFLUXDB_V2_URL', "http://localhost:8086")104 token = os.getenv('INFLUXDB_V2_TOKEN', "my-token")105 timeout = os.getenv('INFLUXDB_V2_TIMEOUT', "10000")106 org = os.getenv('INFLUXDB_V2_ORG', "my-org")107 verify_ssl = os.getenv('INFLUXDB_V2_VERIFY_SSL', "True")108 ssl_ca_cert = os.getenv('INFLUXDB_V2_SSL_CA_CERT', None)109 connection_pool_maxsize = os.getenv('INFLUXDB_V2_CONNECTION_POOL_MAXSIZE', None)110 auth_basic = os.getenv('INFLUXDB_V2_AUTH_BASIC', "False")111 prof = os.getenv("INFLUXDB_V2_PROFILERS", None)112 profilers = None113 if prof is not None:114 profilers = [x.strip() for x in prof.split(',')]115 default_tags = dict()116 for key, value in os.environ.items():117 if key.startswith("INFLUXDB_V2_TAG_"):118 default_tags[key[16:].lower()] = value119 return cls(url, token, debug=debug, timeout=_to_int(timeout), org=org, default_tags=default_tags,120 enable_gzip=enable_gzip, verify_ssl=_to_bool(verify_ssl), ssl_ca_cert=ssl_ca_cert,121 connection_pool_maxsize=_to_int(connection_pool_maxsize), auth_basic=_to_bool(auth_basic),122 profilers=profilers)123# noinspection PyMethodMayBeStatic124class _BaseQueryApi(object):125 default_dialect = Dialect(header=True, delimiter=",", comment_prefix="#",126 annotations=["datatype", "group", "default"], date_time_format="RFC3339")127 def __init__(self, influxdb_client, query_options=None):128 from influxdb_client.client.query_api import QueryOptions129 self._query_options = QueryOptions() if query_options is None else query_options130 self._influxdb_client = influxdb_client131 self._query_api = QueryService(influxdb_client.api_client)132 """Base implementation for Queryable API."""133 def _to_tables(self, response, query_options=None, response_metadata_mode:134 FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> List[FluxTable]:135 """136 Parse HTTP response to FluxTables.137 :param response: HTTP response from an HTTP client. Expected type: `urllib3.response.HTTPResponse`.138 """139 _parser = self._to_tables_parser(response, query_options, response_metadata_mode)140 list(_parser.generator())141 return _parser.table_list()142 async def _to_tables_async(self, response, query_options=None, response_metadata_mode:143 FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> List[FluxTable]:144 """145 Parse HTTP response to FluxTables.146 :param response: HTTP response from an HTTP client. Expected type: `aiohttp.client_reqrep.ClientResponse`.147 """148 async with self._to_tables_parser(response, query_options, response_metadata_mode) as parser:149 async for _ in parser.generator_async():150 pass151 return parser.table_list()152 def _to_csv(self, response: HTTPResponse) -> Iterator[List[str]]:153 """Parse HTTP response to CSV."""154 return csv.reader(codecs.iterdecode(response, _UTF_8_encoding))155 def _to_flux_record_stream(self, response, query_options=None,156 response_metadata_mode: FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> \157 Generator[FluxRecord, Any, None]:158 """159 Parse HTTP response to FluxRecord stream.160 :param response: HTTP response from an HTTP client. Expected type: `urllib3.response.HTTPResponse`.161 """162 _parser = self._to_flux_record_stream_parser(query_options, response, response_metadata_mode)163 return _parser.generator()164 async def _to_flux_record_stream_async(self, response, query_options=None, response_metadata_mode:165 FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> \166 AsyncGenerator['FluxRecord', None]:167 """168 Parse HTTP response to FluxRecord stream.169 :param response: HTTP response from an HTTP client. Expected type: `aiohttp.client_reqrep.ClientResponse`.170 """171 _parser = self._to_flux_record_stream_parser(query_options, response, response_metadata_mode)172 return (await _parser.__aenter__()).generator_async()173 def _to_data_frame_stream(self, data_frame_index, response, query_options=None,174 response_metadata_mode: FluxResponseMetadataMode = FluxResponseMetadataMode.full):175 """176 Parse HTTP response to DataFrame stream.177 :param response: HTTP response from an HTTP client. Expected type: `urllib3.response.HTTPResponse`.178 """179 _parser = self._to_data_frame_stream_parser(data_frame_index, query_options, response, response_metadata_mode)180 return _parser.generator()181 async def _to_data_frame_stream_async(self, data_frame_index, response, query_options=None, response_metadata_mode:182 FluxResponseMetadataMode = FluxResponseMetadataMode.full):183 """184 Parse HTTP response to DataFrame stream.185 :param response: HTTP response from an HTTP client. Expected type: `aiohttp.client_reqrep.ClientResponse`.186 """187 _parser = self._to_data_frame_stream_parser(data_frame_index, query_options, response, response_metadata_mode)188 return (await _parser.__aenter__()).generator_async()189 def _to_tables_parser(self, response, query_options, response_metadata_mode):190 return FluxCsvParser(response=response, serialization_mode=FluxSerializationMode.tables,191 query_options=query_options, response_metadata_mode=response_metadata_mode)192 def _to_flux_record_stream_parser(self, query_options, response, response_metadata_mode):193 return FluxCsvParser(response=response, serialization_mode=FluxSerializationMode.stream,194 query_options=query_options, response_metadata_mode=response_metadata_mode)195 def _to_data_frame_stream_parser(self, data_frame_index, query_options, response, response_metadata_mode):196 return FluxCsvParser(response=response, serialization_mode=FluxSerializationMode.dataFrame,197 data_frame_index=data_frame_index, query_options=query_options,198 response_metadata_mode=response_metadata_mode)199 def _to_data_frames(self, _generator):200 """Parse stream of DataFrames into expected type."""201 from ..extras import pd202 if isinstance(_generator, list):203 _dataFrames = _generator204 else:205 _dataFrames = list(_generator)206 if len(_dataFrames) == 0:207 return pd.DataFrame(columns=[], index=None)208 elif len(_dataFrames) == 1:209 return _dataFrames[0]210 else:211 return _dataFrames212 def _org_param(self, org):213 return get_org_query_param(org=org, client=self._influxdb_client)214 def _get_query_options(self):215 if self._query_options and self._query_options.profilers:216 return self._query_options217 elif self._influxdb_client.profilers:218 from influxdb_client.client.query_api import QueryOptions219 return QueryOptions(profilers=self._influxdb_client.profilers)220 def _create_query(self, query, dialect=default_dialect, params: dict = None):221 query_options = self._get_query_options()222 profilers = query_options.profilers if query_options is not None else None223 q = Query(query=query, dialect=dialect, extern=_BaseQueryApi._build_flux_ast(params, profilers))224 if profilers:225 print("\n===============")226 print("Profiler: query")227 print("===============")228 print(query)229 return q230 @staticmethod231 def _params_to_extern_ast(params: dict) -> List['OptionStatement']:232 statements = []233 for key, value in params.items():234 expression = _BaseQueryApi._parm_to_extern_ast(value)235 if expression is None:236 continue237 statements.append(OptionStatement("OptionStatement",238 VariableAssignment("VariableAssignment", Identifier("Identifier", key),239 expression)))240 return statements241 @staticmethod242 def _parm_to_extern_ast(value) -> Union[Expression, None]:243 if value is None:244 return None245 if isinstance(value, bool):246 return BooleanLiteral("BooleanLiteral", value)247 elif isinstance(value, int):248 return IntegerLiteral("IntegerLiteral", str(value))249 elif isinstance(value, float):250 return FloatLiteral("FloatLiteral", value)251 elif isinstance(value, datetime):252 value = get_date_helper().to_utc(value)253 return DateTimeLiteral("DateTimeLiteral", value.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))254 elif isinstance(value, timedelta):255 _micro_delta = int(value / timedelta(microseconds=1))256 if _micro_delta < 0:257 return UnaryExpression("UnaryExpression", argument=DurationLiteral("DurationLiteral", [258 Duration(magnitude=-_micro_delta, unit="us")]), operator="-")259 else:260 return DurationLiteral("DurationLiteral", [Duration(magnitude=_micro_delta, unit="us")])261 elif isinstance(value, str):262 return StringLiteral("StringLiteral", str(value))263 elif isinstance(value, Iterable):264 return ArrayExpression("ArrayExpression",265 elements=list(map(lambda it: _BaseQueryApi._parm_to_extern_ast(it), value)))266 else:267 return value268 @staticmethod269 def _build_flux_ast(params: dict = None, profilers: List[str] = None):270 imports = []271 body = []272 if profilers is not None and len(profilers) > 0:273 imports.append(ImportDeclaration(274 "ImportDeclaration",275 path=StringLiteral("StringLiteral", "profiler")))276 elements = []277 for profiler in profilers:278 elements.append(StringLiteral("StringLiteral", value=profiler))279 member = MemberExpression(280 "MemberExpression",281 object=Identifier("Identifier", "profiler"),282 _property=Identifier("Identifier", "enabledProfilers"))283 prof = OptionStatement(284 "OptionStatement",285 assignment=MemberAssignment(286 "MemberAssignment",287 member=member,288 init=ArrayExpression(289 "ArrayExpression",290 elements=elements)))291 body.append(prof)292 if params is not None:293 body.extend(_BaseQueryApi._params_to_extern_ast(params))294 return File(package=None, name=None, type=None, imports=imports, body=body)295class _BaseWriteApi(object):296 def __init__(self, influxdb_client, point_settings=None):297 self._influxdb_client = influxdb_client298 self._point_settings = point_settings299 self._write_service = WriteService(influxdb_client.api_client)300 if influxdb_client.default_tags:301 for key, value in influxdb_client.default_tags.items():302 self._point_settings.add_default_tag(key, value)303 def _append_default_tag(self, key, val, record):304 from influxdb_client import Point305 if isinstance(record, bytes) or isinstance(record, str):306 pass307 elif isinstance(record, Point):308 record.tag(key, val)309 elif isinstance(record, dict):310 record.setdefault("tags", {})311 record.get("tags")[key] = val312 elif isinstance(record, Iterable):313 for item in record:314 self._append_default_tag(key, val, item)315 def _append_default_tags(self, record):316 if self._point_settings.defaultTags and record is not None:317 for key, val in self._point_settings.defaultTags.items():318 self._append_default_tag(key, val, record)319 def _serialize(self, record, write_precision, payload, **kwargs):320 from influxdb_client import Point321 if isinstance(record, bytes):322 payload[write_precision].append(record)323 elif isinstance(record, str):324 self._serialize(record.encode(_UTF_8_encoding), write_precision, payload, **kwargs)325 elif isinstance(record, Point):326 precision_from_point = kwargs.get('precision_from_point', True)327 precision = record.write_precision if precision_from_point else write_precision328 self._serialize(record.to_line_protocol(precision=precision), precision, payload, **kwargs)329 elif isinstance(record, dict):330 self._serialize(Point.from_dict(record, write_precision=write_precision, **kwargs),331 write_precision, payload, **kwargs)332 elif 'DataFrame' in type(record).__name__:333 serializer = DataframeSerializer(record, self._point_settings, write_precision, **kwargs)334 self._serialize(serializer.serialize(), write_precision, payload, **kwargs)335 elif hasattr(record, "_asdict"):336 # noinspection PyProtectedMember337 self._serialize(record._asdict(), write_precision, payload, **kwargs)338 elif _HAS_DATACLASS and dataclasses.is_dataclass(record):339 self._serialize(dataclasses.asdict(record), write_precision, payload, **kwargs)340 elif isinstance(record, Iterable):341 for item in record:342 self._serialize(item, write_precision, payload, **kwargs)343# noinspection PyMethodMayBeStatic344class _BaseDeleteApi(object):345 def __init__(self, influxdb_client):346 self._influxdb_client = influxdb_client347 self._service = DeleteService(influxdb_client.api_client)348 def _prepare_predicate_request(self, start, stop, predicate):349 date_helper = get_date_helper()350 if isinstance(start, datetime):351 start = date_helper.to_utc(start)352 if isinstance(stop, datetime):353 stop = date_helper.to_utc(stop)354 predicate_request = DeletePredicateRequest(start=start, stop=stop, predicate=predicate)355 return predicate_request356class _Configuration(Configuration):357 def __init__(self):358 Configuration.__init__(self)359 self.enable_gzip = False360 def update_request_header_params(self, path: str, params: dict):361 super().update_request_header_params(path, params)362 if self.enable_gzip:363 # GZIP Request364 if path == '/api/v2/write':365 params["Content-Encoding"] = "gzip"366 params["Accept-Encoding"] = "identity"367 pass368 # GZIP Response369 if path == '/api/v2/query':370 # params["Content-Encoding"] = "gzip"371 params["Accept-Encoding"] = "gzip"372 pass373 pass374 pass375 def update_request_body(self, path: str, body):376 _body = super().update_request_body(path, body)377 if self.enable_gzip:378 # GZIP Request379 if path == '/api/v2/write':380 import gzip381 if isinstance(_body, bytes):382 return gzip.compress(data=_body)383 else:384 return gzip.compress(bytes(_body, _UTF_8_encoding))385 return _body386def _to_bool(bool_value):387 return str(bool_value).lower() in ("yes", "true")388def _to_int(int_value):...
pyhotvect.py
Source:pyhotvect.py
1import json2import logging3import os4from datetime import datetime, tzinfo, timedelta5from shutil import copyfile6from typing import Dict, List, Any7import hotvect.mlutils as mlutils8import pandas as pd9from hotvect.utils import trydelete, runshell, read_json, to_zip_archive, ensure_file_exists, clean_dir, \10 ensure_dir_exists11logging.basicConfig(level=logging.WARNING)12# Courtesy of https://stackoverflow.com/a/23705687/23490113# Under CC BY-SA 3.014class SimpleUTC(tzinfo):15 def tzname(self, **kwargs):16 return "UTC"17 def utcoffset(self, dt):18 return timedelta(0)19class Hotvect:20 def __init__(self,21 hotvect_util_jar_path: str,22 algorithm_jar_path: str,23 metadata_base_path: str,24 output_base_path: str,25 train_data_path: str,26 validation_data_path: str,27 algorithm_definition: Dict[str, Any],28 state_source_base_path: str = None,29 run_id: str = "default",30 enable_gzip: bool = True,31 jvm_options: List[str] = None32 ):33 self.algorithm_definition = algorithm_definition34 self.run_id: str = run_id35 self.ran_at: str = datetime.utcnow().replace(tzinfo=SimpleUTC()).isoformat()36 # Utilities37 self.hotvect_util_jar_path = hotvect_util_jar_path38 # Algorithm classes39 self.algorithm_jar_path = algorithm_jar_path40 # Metadata41 self.metadata_base_path = metadata_base_path42 # Output43 self.output_base_path = output_base_path44 # Train data45 self.train_data_path = train_data_path46 ensure_file_exists(train_data_path)47 # Validation data48 self.validation_data_location = validation_data_path49 ensure_file_exists(validation_data_path)50 # State source data51 self.state_source_base_path = state_source_base_path52 if self.state_source_base_path:53 ensure_dir_exists(self.state_source_base_path)54 self.feature_states: Dict[str, str] = {}55 # Gzip56 self.enable_gzip = enable_gzip57 # Jvm options58 if not jvm_options:59 self.jvm_options = ['-Xmx4g']60 else:61 self.jvm_options = jvm_options62 logging.info(f'Initialized:{self.__dict__}')63 def set_run_id(self, run_id: str):64 self.run_id = run_id65 def set_algorithm_definition(self, algorithm_definition: Dict[str, Any]):66 self.algorithm_definition = algorithm_definition67 def metadata_path(self) -> str:68 return os.path.join(self.metadata_base_path, self.algorithm_definition['algorithm_name'], self.run_id)69 def output_path(self) -> str:70 return os.path.join(self.output_base_path, self.algorithm_definition['algorithm_name'], self.run_id)71 def state_output_path(self, state_name: str):72 state_filename = f'{state_name}'73 return os.path.join(74 self.output_path(),75 state_filename76 )77 def encoded_data_file_path(self) -> str:78 encode_suffix = 'encoded.gz' if self.enable_gzip else 'encoded'79 return os.path.join(80 self.output_path(),81 encode_suffix82 )83 def model_file_path(self) -> str:84 return os.path.join(85 self.output_path(),86 'model.parameter'87 )88 def score_file_path(self) -> str:89 return os.path.join(90 self.output_path(),91 'validation_scores.csv'92 )93 def audit_data_file_path(self) -> str:94 encode_suffix = 'audit.jsonl.gz' if self.enable_gzip else 'audit.jsonl'95 return os.path.join(96 self.output_path(),97 encode_suffix98 )99 def predict_parameter_file_path(self) -> str:100 return os.path.join(101 self.output_path(),102 f"{self.algorithm_definition['algorithm_name']}@{self.run_id}.parameters.zip"103 )104 def encode_parameter_file_path(self) -> str:105 return os.path.join(106 self.output_path(),107 'encode.parameters.zip'108 )109 def _write_algorithm_definition(self) -> str:110 """Write algorithm definition so that Java can read it"""111 algorithm_definition_path = os.path.join(112 self.metadata_path(),113 'algorithm_definition.json'114 )115 trydelete(algorithm_definition_path)116 with open(algorithm_definition_path, 'w') as fp:117 json.dump(self.algorithm_definition, fp)118 return algorithm_definition_path119 def _write_data(self, data: Dict, dest_file_name: str) -> str:120 """Write algorithm definition so that Java can read it"""121 dest = os.path.join(122 self.output_path(),123 dest_file_name124 )125 trydelete(dest)126 with open(dest, 'w') as fp:127 json.dump(data, fp)128 return dest129 def clean(self) -> None:130 for file in [131 self.encoded_data_file_path(),132 self.model_file_path(),133 self.score_file_path()134 ]:135 trydelete(file)136 for directory in [137 self.metadata_path(),138 self.output_path(),139 ]:140 clean_dir(directory)141 logging.info('Cleaned output and metadata')142 def run_all(self, run_id=None, clean=True) -> Dict:143 if run_id:144 self.run_id = run_id145 result = {146 'algorithm_name': self.algorithm_definition['algorithm_name'],147 'run_id': self.run_id,148 'ran_at': self.ran_at,149 'algorithm_definition': self.algorithm_definition150 }151 self.clean()152 result['states'] = self.generate_states()153 result['package_encode_params'] = self.package_encode_parameters()154 result['encode'] = self.encode()155 result['train'] = self.train()156 result['package_predict_params'] = self.package_predict_parameters()157 result['predict'] = self.predict()158 result['evaluate'] = self.evaluate()159 if clean:160 self.clean_output()161 return result162 def _base_command(self, metadata_location: str) -> List[str]:163 ret = [164 'java',165 '-cp', f"{self.hotvect_util_jar_path}",166 ]167 ret.extend(self.jvm_options)168 ret.extend(['com.eshioji.hotvect.commandline.Main',169 '--algorithm-jar', f'{self.algorithm_jar_path}',170 '--algorithm-definition', self._write_algorithm_definition(),171 '--meta-data', metadata_location])172 return ret173 def generate_states(self) -> Dict:174 states = self.algorithm_definition.get('vectorizer_parameters', {}).get('feature_states', {})175 metadata = {}176 for state_name, instruction in states.items():177 metadata_path = os.path.join(self.metadata_path(), f'generate-state-{state_name}.json')178 trydelete(metadata_path)179 output_path = self.state_output_path(state_name)180 trydelete(output_path)181 source_path = os.path.join(self.state_source_base_path, instruction['source_name'])182 ensure_file_exists(source_path)183 generation_task = instruction['generation_task']184 if instruction.get('cache'):185 cached = os.path.join(self.state_source_base_path, instruction['cache'])186 ensure_file_exists(cached)187 logging.info(f'Using cache for state:{state_name} from {cached}')188 copyfile(cached, output_path)189 metadata[state_name] = {190 'cache': cached191 }192 else:193 logging.info(f'No cache found for state:{state_name}, generating')194 cmd = self._base_command(metadata_path)195 cmd.extend(['--generate-state', generation_task,196 '--training-data', self.train_data_path,197 '--source', source_path,198 '--dest', output_path])199 runshell(cmd)200 metadata[state_name] = read_json(metadata_path)201 self.feature_states[state_name] = output_path202 return metadata203 def package_encode_parameters(self) -> Dict:204 encode_parameter_package_location = self.encode_parameter_file_path()205 trydelete(encode_parameter_package_location)206 to_package = list(self.feature_states.values())207 to_zip_archive(to_package, encode_parameter_package_location)208 return {209 'sources': to_package,210 'package': encode_parameter_package_location211 }212 def encode(self) -> Dict:213 metadata_location = os.path.join(self.metadata_path(), 'encode_metadata.json')214 trydelete(metadata_location)215 encoded_data_location = self.encoded_data_file_path()216 trydelete(encoded_data_location)217 cmd = self._base_command(metadata_location)218 cmd.append('--encode')219 if self.feature_states:220 # We have feature states221 cmd.extend(['--parameters', self.encode_parameter_file_path()])222 cmd.extend(['--source', self.train_data_path])223 cmd.extend(['--dest', encoded_data_location])224 runshell(cmd)225 return read_json(metadata_location)226 def train(self) -> Dict:227 metadata_location = os.path.join(self.metadata_path(), 'train_metadata.json')228 trydelete(metadata_location)229 model_location = self.model_file_path()230 trydelete(model_location)231 cmd = [232 'vw', self.encoded_data_file_path(),233 '--readable_model', model_location,234 '--noconstant',235 '--loss_function', 'logistic',236 ]237 train_params = self.algorithm_definition['training_parameters']238 cmd.extend(train_params)239 train_log = runshell(cmd)240 metadata = {241 'training_parameters': train_params,242 'train_log': train_log243 }244 with open(metadata_location, 'w') as f:245 json.dump(metadata, f)246 return metadata247 def package_predict_parameters(self) -> Dict:248 predict_parameter_package_location = self.predict_parameter_file_path()249 trydelete(predict_parameter_package_location)250 # Add the model file251 to_package = [self.model_file_path()]252 # Add all the feature states253 to_package.extend(list(self.feature_states.values()))254 # Add the algo parameters255 algorithm_parameters = {256 'algorithm_name': self.algorithm_definition['algorithm_name'],257 'parameter_id': self.run_id,258 'ran_at': self.ran_at,259 'algorithm_definition': self.algorithm_definition,260 'sources': to_package,261 'package': predict_parameter_package_location262 }263 algo_parameters_path = self._write_data(algorithm_parameters, 'algorithm_parameters.json')264 to_package.append(algo_parameters_path)265 to_zip_archive(to_package, predict_parameter_package_location)266 return algorithm_parameters267 def predict(self) -> Dict:268 metadata_location = os.path.join(self.metadata_path(), 'predict_metadata.json')269 trydelete(metadata_location)270 score_location = self.score_file_path()271 trydelete(score_location)272 cmd = self._base_command(metadata_location)273 cmd.append('--predict')274 cmd.extend(['--source', self.validation_data_location])275 cmd.extend(['--dest', score_location])276 cmd.extend(['--parameters', self.predict_parameter_file_path()])277 runshell(cmd)278 return read_json(metadata_location)279 def evaluate(self) -> Dict:280 metadata_location = os.path.join(self.metadata_path(), 'evaluate_metadata.json')281 trydelete(metadata_location)282 df = pd.read_csv(self.score_file_path(), header=None)283 lower_auc, mean_auc, upper_auc = mlutils.bootstrap_roc_auc(df[1], df[0])284 meta_data = {285 'upper_auc': upper_auc,286 'mean_auc': mean_auc,287 'lower_auc': lower_auc288 }289 with open(metadata_location, 'w') as f:290 json.dump(meta_data, f)291 return meta_data292 def clean_output(self) -> None:293 trydelete(self.encoded_data_file_path())294 trydelete(self.model_file_path())295 trydelete(self.score_file_path())296 trydelete(self.output_path())297 def audit(self) -> None:298 metadata_location = os.path.join(self.metadata_path(), 'audit_metadata.json')299 trydelete(metadata_location)300 audit_data_location = self.audit_data_file_path()301 trydelete(audit_data_location)302 cmd = self._base_command(metadata_location)303 cmd.append('--audit')304 if self.feature_states:305 # We have feature states306 cmd.extend(['--parameters', self.encode_parameter_file_path()])307 cmd.extend(['--source', self.train_data_path])308 cmd.extend(['--dest', audit_data_location])309 runshell(cmd)...
influxdb_client.py
Source:influxdb_client.py
1"""InfluxDBClient is client for API defined in https://github.com/influxdata/influxdb/blob/master/http/swagger.yml."""2from __future__ import absolute_import3import configparser4import os5from influxdb_client import Configuration, ApiClient, HealthCheck, HealthService, Ready, ReadyService6from influxdb_client.client.authorizations_api import AuthorizationsApi7from influxdb_client.client.bucket_api import BucketsApi8from influxdb_client.client.delete_api import DeleteApi9from influxdb_client.client.labels_api import LabelsApi10from influxdb_client.client.organizations_api import OrganizationsApi11from influxdb_client.client.query_api import QueryApi12from influxdb_client.client.tasks_api import TasksApi13from influxdb_client.client.users_api import UsersApi14from influxdb_client.client.write_api import WriteApi, WriteOptions, PointSettings15class InfluxDBClient(object):16 """InfluxDBClient is client for InfluxDB v2."""17 def __init__(self, url, token, debug=None, timeout=10_000, enable_gzip=False, org: str = None,18 default_tags: dict = None, **kwargs) -> None:19 """20 Initialize defaults.21 :param url: InfluxDB server API url (ex. http://localhost:8086).22 :param token: auth token23 :param debug: enable verbose logging of http requests24 :param timeout: HTTP client timeout setting for a request specified in milliseconds.25 If one number provided, it will be total request timeout.26 It can also be a pair (tuple) of (connection, read) timeouts.27 :param enable_gzip: Enable Gzip compression for http requests. Currently only the "Write" and "Query" endpoints28 supports the Gzip compression.29 :param org: organization name (used as a default in query and write API)30 :key bool verify_ssl: Set this to false to skip verifying SSL certificate when calling API from https server.31 :key str ssl_ca_cert: Set this to customize the certificate file to verify the peer.32 :key str proxy: Set this to configure the http proxy to be used (ex. http://localhost:3128)33 :key int connection_pool_maxsize: Number of connections to save that can be reused by urllib3.34 Defaults to "multiprocessing.cpu_count() * 5".35 :key urllib3.util.retry.Retry retries: Set the default retry strategy that is used for all HTTP requests36 except batching writes. As a default there is no one retry strategy.37 """38 self.url = url39 self.token = token40 self.org = org41 self.default_tags = default_tags42 conf = _Configuration()43 if self.url.endswith("/"):44 conf.host = self.url[:-1]45 else:46 conf.host = self.url47 conf.enable_gzip = enable_gzip48 conf.debug = debug49 conf.verify_ssl = kwargs.get('verify_ssl', True)50 conf.ssl_ca_cert = kwargs.get('ssl_ca_cert', None)51 conf.proxy = kwargs.get('proxy', None)52 conf.connection_pool_maxsize = kwargs.get('connection_pool_maxsize', conf.connection_pool_maxsize)53 conf.timeout = timeout54 auth_token = self.token55 auth_header_name = "Authorization"56 auth_header_value = "Token " + auth_token57 retries = kwargs.get('retries', False)58 self.api_client = ApiClient(configuration=conf, header_name=auth_header_name,59 header_value=auth_header_value, retries=retries)60 def __enter__(self):61 """62 Enter the runtime context related to this object.63 It will bind this methodâs return value to the target(s)64 specified in the `as` clause of the statement.65 return: self instance66 """67 return self68 def __exit__(self, exc_type, exc_value, traceback):69 """Exit the runtime context related to this object and close the client."""70 self.close()71 @classmethod72 def from_config_file(cls, config_file: str = "config.ini", debug=None, enable_gzip=False):73 """74 Configure client via configuration file. The configuration has to be under 'influx' section.75 The supported formats:76 - https://docs.python.org/3/library/configparser.html77 - https://toml.io/en/78 Configuration options:79 - url80 - org81 - token82 - timeout,83 - verify_ssl84 - ssl_ca_cert85 - connection_pool_maxsize86 config.ini example::87 [influx2]88 url=http://localhost:808689 org=my-org90 token=my-token91 timeout=600092 connection_pool_maxsize=2593 [tags]94 id = 132-987-65595 customer = California Miner96 data_center = ${env.data_center}97 config.toml example::98 [influx2]99 url = "http://localhost:8086"100 token = "my-token"101 org = "my-org"102 timeout = 6000103 connection_pool_maxsize = 25104 [tags]105 id = "132-987-655"106 customer = "California Miner"107 data_center = "${env.data_center}"108 """109 config = configparser.ConfigParser()110 config.read(config_file)111 def config_value(key: str):112 return config['influx2'][key].strip('"')113 url = config_value('url')114 token = config_value('token')115 timeout = None116 if config.has_option('influx2', 'timeout'):117 timeout = config_value('timeout')118 org = None119 if config.has_option('influx2', 'org'):120 org = config_value('org')121 verify_ssl = True122 if config.has_option('influx2', 'verify_ssl'):123 verify_ssl = config_value('verify_ssl')124 ssl_ca_cert = None125 if config.has_option('influx2', 'ssl_ca_cert'):126 ssl_ca_cert = config_value('ssl_ca_cert')127 connection_pool_maxsize = None128 if config.has_option('influx2', 'connection_pool_maxsize'):129 connection_pool_maxsize = config_value('connection_pool_maxsize')130 default_tags = None131 if config.has_section('tags'):132 tags = {k: v.strip('"') for k, v in config.items('tags')}133 default_tags = dict(tags)134 return cls(url, token, debug=debug, timeout=_to_int(timeout), org=org, default_tags=default_tags,135 enable_gzip=enable_gzip, verify_ssl=_to_bool(verify_ssl), ssl_ca_cert=ssl_ca_cert,136 connection_pool_maxsize=_to_int(connection_pool_maxsize))137 @classmethod138 def from_env_properties(cls, debug=None, enable_gzip=False):139 """140 Configure client via environment properties.141 Supported environment properties:142 - INFLUXDB_V2_URL143 - INFLUXDB_V2_ORG144 - INFLUXDB_V2_TOKEN145 - INFLUXDB_V2_TIMEOUT146 - INFLUXDB_V2_VERIFY_SSL147 - INFLUXDB_V2_SSL_CA_CERT148 - INFLUXDB_V2_CONNECTION_POOL_MAXSIZE149 """150 url = os.getenv('INFLUXDB_V2_URL', "http://localhost:8086")151 token = os.getenv('INFLUXDB_V2_TOKEN', "my-token")152 timeout = os.getenv('INFLUXDB_V2_TIMEOUT', "10000")153 org = os.getenv('INFLUXDB_V2_ORG', "my-org")154 verify_ssl = os.getenv('INFLUXDB_V2_VERIFY_SSL', "True")155 ssl_ca_cert = os.getenv('INFLUXDB_V2_SSL_CA_CERT', None)156 connection_pool_maxsize = os.getenv('INFLUXDB_V2_CONNECTION_POOL_MAXSIZE', None)157 default_tags = dict()158 for key, value in os.environ.items():159 if key.startswith("INFLUXDB_V2_TAG_"):160 default_tags[key[16:].lower()] = value161 return cls(url, token, debug=debug, timeout=_to_int(timeout), org=org, default_tags=default_tags,162 enable_gzip=enable_gzip, verify_ssl=_to_bool(verify_ssl), ssl_ca_cert=ssl_ca_cert,163 connection_pool_maxsize=_to_int(connection_pool_maxsize))164 def write_api(self, write_options=WriteOptions(), point_settings=PointSettings()) -> WriteApi:165 """166 Create a Write API instance.167 :param point_settings:168 :param write_options: write api configuration169 :return: write api instance170 """171 return WriteApi(influxdb_client=self, write_options=write_options, point_settings=point_settings)172 def query_api(self) -> QueryApi:173 """174 Create a Query API instance.175 :return: Query api instance176 """177 return QueryApi(self)178 def close(self):179 """Shutdown the client."""180 self.__del__()181 def __del__(self):182 """Shutdown the client."""183 if self.api_client:184 self.api_client.__del__()185 self.api_client = None186 def buckets_api(self) -> BucketsApi:187 """188 Create the Bucket API instance.189 :return: buckets api190 """191 return BucketsApi(self)192 def authorizations_api(self) -> AuthorizationsApi:193 """194 Create the Authorizations API instance.195 :return: authorizations api196 """197 return AuthorizationsApi(self)198 def users_api(self) -> UsersApi:199 """200 Create the Users API instance.201 :return: users api202 """203 return UsersApi(self)204 def organizations_api(self) -> OrganizationsApi:205 """206 Create the Organizations API instance.207 :return: organizations api208 """209 return OrganizationsApi(self)210 def tasks_api(self) -> TasksApi:211 """212 Create the Tasks API instance.213 :return: tasks api214 """215 return TasksApi(self)216 def labels_api(self) -> LabelsApi:217 """218 Create the Labels API instance.219 :return: labels api220 """221 return LabelsApi(self)222 def health(self) -> HealthCheck:223 """224 Get the health of an instance.225 :return: HealthCheck226 """227 health_service = HealthService(self.api_client)228 try:229 health = health_service.get_health()230 return health231 except Exception as e:232 return HealthCheck(name="influxdb", message=str(e), status="fail")233 def ready(self) -> Ready:234 """235 Get The readiness of the InfluxDB 2.0.236 :return: Ready237 """238 ready_service = ReadyService(self.api_client)239 return ready_service.get_ready()240 def delete_api(self) -> DeleteApi:241 """242 Get the delete metrics API instance.243 :return: delete api244 """245 return DeleteApi(self)246class _Configuration(Configuration):247 def __init__(self):248 Configuration.__init__(self)249 self.enable_gzip = False250 def update_request_header_params(self, path: str, params: dict):251 super().update_request_header_params(path, params)252 if self.enable_gzip:253 # GZIP Request254 if path == '/api/v2/write':255 params["Content-Encoding"] = "gzip"256 params["Accept-Encoding"] = "identity"257 pass258 # GZIP Response259 if path == '/api/v2/query':260 # params["Content-Encoding"] = "gzip"261 params["Accept-Encoding"] = "gzip"262 pass263 pass264 pass265 def update_request_body(self, path: str, body):266 _body = super().update_request_body(path, body)267 if self.enable_gzip:268 # GZIP Request269 if path == '/api/v2/write':270 import gzip271 if isinstance(_body, bytes):272 return gzip.compress(data=_body)273 else:274 return gzip.compress(bytes(_body, "utf-8"))275 return _body276def _to_bool(bool_value):277 return str(bool_value).lower() in ("yes", "true")278def _to_int(int_value):...
configure_caddy.py
Source:configure_caddy.py
1#!/usr/bin/python2"""3Configure caddy service4"""5import os6import sys7import json8import bcrypt9import logging10import coloredlogs11import argparse12from urllib.parse import quote, urljoin13from subprocess import run, call14import functions as func15### Enable logging16logging.basicConfig(17 format='%(asctime)s [%(levelname)s] %(message)s', 18 level=logging.INFO, 19 stream=sys.stdout)20log = logging.getLogger(__name__)21### Enable argument parsing22parser = argparse.ArgumentParser()23parser.add_argument('--opts', type=json.loads, help='Set script arguments')24parser.add_argument('--env', type=json.loads, help='Set script environment')25parser.add_argument('--user', type=json.loads, help='Load user settings')26parser.add_argument('--settings', type=json.loads, help='Load script settings')27args, unknown = parser.parse_known_args()28if unknown:29 log.error("Unknown arguments " + str(unknown))30### Load arguments31cli_opts = args.opts32cli_env = args.env33cli_user = args.user34cli_settings = args.settings35### Set log level36verbosity = cli_opts.get("verbosity")37log.setLevel(verbosity)38# Setup colored console logs39coloredlogs.install(fmt='%(asctime)s [%(levelname)s] %(message)s', level=verbosity, logger=log)40### Get envs41proxy_base_url = cli_env.get("PROXY_BASE_URL")42caddy_virtual_port = cli_env.get("CADDY_VIRTUAL_PORT")43caddy_virtual_host = cli_env.get("CADDY_VIRTUAL_HOST")44caddy_virtual_proto = cli_env.get("CADDY_VIRTUAL_PROTO")45caddy_virtual_base_url = cli_env.get("CADDY_VIRTUAL_BASE_URL")46caddy_proxy_encodings_gzip = cli_env.get("CADDY_PROXY_ENCODINGS_GZIP")47caddy_proxy_encodings_zstd = cli_env.get("CADDY_PROXY_ENCODINGS_ZSTD")48caddy_proxy_templates = cli_env.get("CADDY_PROXY_TEMPLATES")49caddy_letsencrypt_email = cli_env.get("CADDY_LETSENCRYPT_EMAIL")50caddy_letsencrypt_endpoint = cli_env.get("CADDY_LETSENCRYPT_ENDPOINT")51caddy_http_port = cli_env.get("CADDY_HTTP_PORT")52caddy_https_port = cli_env.get("CADDY_HTTPS_PORT")53caddy_auto_https = cli_env.get("CADDY_AUTO_HTTPS")54fb_port = cli_user.get("filebrowser").get("port")55fb_base_url = cli_user.get("filebrowser").get("base_url")56vscode_bind_addr = cli_user.get("vscode").get("bind_addr")57vscode_base_url = cli_user.get("vscode").get("base_url")58app_bind_addr = cli_user.get("app").get("bind_addr")59app_base_url = cli_user.get("app").get("base_url")60### Get user settings61user_name = cli_user.get("name")62user_group = cli_user.get("group")63user_home = cli_user.get("dirs").get("home").get("path")64### Clean up envs65application = "caddy"66proxy_base_url = func.clean_url(proxy_base_url)67host_fqdn = caddy_virtual_host # @TODO: Not reading from env68host_port = caddy_virtual_port69host_ip = "0.0.0.0"70host_proto = caddy_virtual_proto71host_base_url = func.clean_url(caddy_virtual_base_url)72auto_https = True if caddy_auto_https == "true" else False73enable_gzip = True if caddy_proxy_encodings_gzip == "true" else False74enable_zstd = True if caddy_proxy_encodings_zstd == "true" else False75enable_templates = True if caddy_proxy_templates == "true" else False76### Set config and data paths77config_dir = os.path.join(user_home, ".config", application)78if not os.path.exists(config_dir):79 os.makedirs(config_dir)80storage = os.path.join(config_dir, "storage")81if not os.path.exists(storage): 82 os.mkdir(storage)83### Set certificate endpoint84letsencrypt_staging = "https://acme-staging-v02.api.letsencrypt.org/directory"85letsencrypt_production = "https://acme-v02.api.letsencrypt.org/directory"86if caddy_letsencrypt_endpoint == "dev":87 endpoint = letsencrypt_staging88elif caddy_letsencrypt_endpoint == "prod":89 endpoint = letsencrypt_production90elif caddy_letsencrypt_endpoint == "internal":91 #@TODO: Get internal certs working92 endpoint = letsencrypt_production = "set this up"93else:94 log.info(f"invalid letsencrypt endpoint: '{caddy_letsencrypt_endpoint}'")95### Run protocol check96if not host_proto in ['http', 'https']:97 log.critical(f"{application}: protocol '{proto}' is not valid! Exiting.")98 sys.exit()99### Define application route settings100servers = dict()101servers["automatic_https"]: auto_https102servers['default'] = dict()103domains = dict()104domains[host_fqdn] = ""105vscode_settings = {106 "name": "vscode",107 "host": "localhost",108 "port": vscode_bind_addr.split(":",1)[1],109 "proto": "http",110 "base_url": func.clean_url(vscode_base_url),111 "enable_gzip": True,112 "enable_gzip": True,113 "enable_templates": True,114}115filebrowser_settings = {116 "name": "filebrowser",117 "host": "localhost",118 "port": fb_port,119 "proto": "http",120 "base_url": func.clean_url(fb_base_url),121 "enable_gzip": True,122 "enable_gzip": True,123 "enable_templates": True,124}125app_settings = {126 "name": "app",127 "host": "localhost",128 "port": app_bind_addr.split(":",1)[1],129 "proto": "http",130 "base_url": func.clean_url(app_base_url),131 "enable_gzip": True,132 "enable_gzip": True,133 "enable_templates": True,134}135### Create application sub-config templates136service_settings = [vscode_settings, filebrowser_settings, app_settings]137subroutes = list()138for service in service_settings:139 service_base_url = urljoin(host_base_url, service.get("base_url"))140 full_base_url = urljoin(proxy_base_url, service_base_url) if service_base_url != "/" else ""141 log.info("{name} base url: '{url}'".format(name=service.get("name"), url=full_base_url))142 encodings = dict()143 if service.get("enable_gzip") or service.get("enable_zstd"):144 encodings = {145 "handle": [{146 "encodings": {},147 "handler": "encode"148 }]149 }150 if service.get("enable_gzip"):151 encodings["handle"][0]["encodings"]['gzip'] = dict()152 if service.get("enable_zstd"):153 encodings["handle"][0]["encodings"]['zstd']= dict()154 templates = dict()155 if service.get("enable_templates"):156 templates = {157 "handle": [{158 "handler": "templates"159 }]160 }161 subroute = {162 "handler": "subroute",163 "routes": [{164 "handle": [{165 "handler": "static_response",166 "headers": {167 "Location": [168 f"{full_base_url}/"169 ]170 },171 "status_code": 302172 }173 ],174 "match": [{175 "path": [176 f"{full_base_url}"177 ]178 }179 ]180 },181 {182 "handle": [{183 "handler": "subroute",184 "routes": [{185 "handle": [{186 "handler": "rewrite",187 "strip_path_prefix": f"{full_base_url}"188 }]189 },190 {191 "handle": [{192 "handler": "reverse_proxy",193 "upstreams": [{194 "dial": "{}:{}".format(service.get("host"), service.get("port"))195 }]196 }]197 },198 encodings,199 templates200 ]201 }],202 "match": [{203 "path": [204 f"{full_base_url}/*"205 ]206 }]207 }]208 }209 subroutes.append(subroute)210if host_fqdn != None:211 if host_fqdn == "":212 match = []213 else:214 match = [{ 215 "host": [host_fqdn]216 }]217 route = {218 "match": match,219 "handle": subroutes,220 "terminal": True221 }222if servers['default'].get('routes') == None:223 servers['default']['listen'] = [f"{host_ip}:{host_port}"]224 servers['default']['routes'] = [route]225 servers['default']['logs'] = {226 "logger_names": {227 host_fqdn: "common",228 }229 }230else:231 servers['default']['routes'].append(route)232### Create config template233config_file = {234 "admin": {235 "disabled": False,236 "listen": '',237 "enforce_origin": False,238 "origins": [''],239 "config": {240 "persist": False241 }242 },243 "logging": {244 "logs": {245 "default": {246 "exclude": [247 "http.log.access.json",248 "http.log.access.common",249 "http.log.access.common_and_json"250 ]251 },252 "common": {253 "writer": {254 "output": "stdout"255 },256 "encoder": {257 "format": "single_field",258 "field": "common_log"259 },260 "level": "",261 "sampling": {262 "interval": 0,263 "first": 0,264 "thereafter": 0265 },266 "include": ["http.log.access.common"],267 }268 }269 },270 "storage": {271 "module": "file_system",272 "root": storage273 },274 "apps": {275 "http": {276 "http_port": int(caddy_http_port),277 "https_port": int(caddy_https_port),278 "servers": servers279 },280 "tls": {281 "automation": {282 "policies": [{283 "subjects": list(domains.keys()),284 "issuers": [285 {286 "module": "acme",287 "ca": endpoint,288 "email": caddy_letsencrypt_email289 },290 {291 "module": "internal",292 "ca": "",293 "lifetime": 0,294 "sign_with_root": False295 }296 ],297 "key_type": ""298 }]299 }300 }301 }302}303### Write config file304config_path = os.path.join(config_dir, "settings.json")305config_json = json.dumps(config_file, indent = 4)306with open(config_path, "w") as f: 307 f.write(config_json)308# fix permissions309log.info(f"setting permissions on '{config_dir}' to '{user_name}:{user_group}'")310func.recursive_chown(config_dir, user_name, user_group)311### Display final config312log.debug(f"{application} config: '{config_path}'")...
LambdaTest’s Playwright tutorial will give you a broader idea about the Playwright automation framework, its unique features, and use cases with examples to exceed your understanding of Playwright testing. This tutorial will give A to Z guidance, from installing the Playwright framework to some best practices and advanced concepts.
Get 100 minutes of automation test minutes FREE!!