diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml
index 3f8a800..1f195b7 100644
--- a/.github/workflows/push.yml
+++ b/.github/workflows/push.yml
@@ -11,8 +11,8 @@ on:
concurrency: ci-${{ github.ref }} # to avoid tag collisions in the ECR
env:
# repository variables:
- KBC_DEVELOPERPORTAL_APP: "kds-team.app-custom-python" # replace with your component id
- KBC_DEVELOPERPORTAL_VENDOR: "kds-team" # replace with your vendor
+ KBC_DEVELOPERPORTAL_APP: "kds-team.app-custom-python"
+ KBC_DEVELOPERPORTAL_VENDOR: "kds-team"
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USER }}
KBC_DEVELOPERPORTAL_USERNAME: "kds-team+github"
@@ -230,4 +230,4 @@ jobs:
- name: Update developer portal properties
run: |
chmod +x scripts/developer_portal/*.sh
- scripts/developer_portal/update_properties.sh
\ No newline at end of file
+ scripts/developer_portal/update_properties.sh
diff --git a/.ssh/known_hosts b/.ssh/known_hosts
new file mode 100644
index 0000000..0ec72b2
--- /dev/null
+++ b/.ssh/known_hosts
@@ -0,0 +1,12 @@
+# In case public repositories update their keys, the component will start failing with:
+# Host key verification failed.
+# fatal: Could not read from remote repository.
+#
+# In such case, retrieve the new keys using the following command:
+# ssh-keyscan -t ed25519 github.com
+
+# github.com:22 SSH-2.0-f892a94b
+github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
+
+# bitbucket.org:22 SSH-2.0-conker_74c0242eb7-dirty 787a0a0e3e79
+bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO
diff --git a/Dockerfile b/Dockerfile
index 0a6352a..db4a044 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,9 +16,25 @@ ENV UV_CACHE_DIR="/.cache/uv"
# Using the same path as venv defined in the base image so we can use all the preinstalled packages
ENV UV_PROJECT_ENVIRONMENT="/home/default/"
-# Run uv sync as uid/gid 1000 so we don't have to chown the /home/default directory with 100k files =-O
+# Preinstall other Python versions for creating isolated virtual environments
+USER 1000:1000
+RUN uv python install 3.12
+RUN uv python install 3.13
+RUN uv python install 3.14
+
+# Add Github SSH host key to known_hosts file & create .bash_aliases for convenience when debugging
USER 1000:1000
+RUN mkdir /home/${USERNAME}/.ssh
+COPY .ssh/known_hosts /home/${USERNAME}/.ssh/known_hosts
+RUN echo "alias l='ls -Al --group-directories-first'" >> /home/${USERNAME}/.bash_aliases
+# Create root's .ssh directory for storing SSH keys when running sync actions
+USER root
+RUN mkdir /root/.ssh
+COPY .ssh/known_hosts /root/.ssh/known_hosts
+
+# Run uv sync as uid/gid 1000 so we don't have to chown the /home/default directory with 100k files =-O
+USER 1000:1000
WORKDIR /code/
COPY pyproject.toml .
COPY uv.lock .
diff --git a/README.md b/README.md
index 2ab86d0..857e2ba 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,14 @@
+- [Custom Python Component](#custom-python-component)
+ - [Configuration](#configuration)
+ - [Git configuration](#git-configuration)
+ - [SSH configuration](#ssh-configuration)
+ - [Example: Running code saved in custom repository + template đź§©](#example-running-code-saved-in-custom-repository--template-)
+ - [Example: Listing preinstalled packages](#example-listing-preinstalled-packages)
+ - [Example: Accessing custom configuration parameters](#example-accessing-custom-configuration-parameters)
+ - [Development](#development)
+ - [Integration](#integration)
+
+
# Custom Python Component
This component lets you run your own Python code directly within Keboola, with support for custom dependencies configured via the UI.
@@ -5,11 +16,61 @@ This component lets you run your own Python code directly within Keboola, with s
## Configuration
-- `code`: JSON encoded Python code to run.
-- `packages`: Array of extra packages to be installed.
+- `source`: Source of the code to run.
+ - `code`: Custom code entered in a text field (default).
+ - `git`: Custom repository.
- `user_properties`: Object containing custom configuration parameters. The key names prefixed with `#` will be encrypted upon saving.
+- `git`: Object containing configuration of the git repository, which shall be cloned and run (`"source": "git"` only).
+- `code`: JSON encoded Python code to run (`"source": "code"` only).
+- `packages`: Array of extra packages to be installed (`"source": "code"` only). *If you're not sure whether you need to install certain package or not, you can run the command `uv pip list` via subprocess (see the example below).*
+
+
+### Git configuration
+
+The git configuration object supports the following parameters:
+
+- `url`: Repository URL – supports both HTTPS and SSH formats.
+- `branch`: Branch name to checkout – UI provides branch selection.
+- `filename`: Python script filename to execute – UI lists available files.
+- `auth`: Repository visibility & authentication method.
+ - `none`: Public repository, no authentication (default).
+ - `pat`: Private repository, Personal Access Token.
+ - `ssh`: Private repository, SSH key.
+- `#token`: Personal Access Token (`"auth": "pat"` only). This value will be encrypted in Keboola Storage.
+- `ssh_keys`: SSH keys configuration object (`"auth": "ssh"` only).
+
+
+### SSH configuration
+
+- `keys`: Object containing both public and private keys.
+ - `public`: Public key saved in your Git project. This value is not passed by the component and is saved just for future reference.
+ - `#private`: Private key used for authentication. This value will be encrypted in Keboola Storage.
+
+
+### Example: Running code saved in custom repository + template đź§©
+
+As this might become a preferred way of running custom Python code in Keboola for many, we prepared a [simple example project](https://github.com/keboola/component-custom-python-example-repo-1), which help you with your first steps (and can also server you as a template for any of your future projects).
+
+
+Contents of the `config.json` file:
+
+```json
+{
+ "parameters": {
+ "source": "git",
+ "git": {
+ "url": "https://github.com/keboola/component-custom-python-example-repo-1.git",
+ "branch": "main",
+ "filename": "main.py",
+ "auth": "none",
+ },
+ "user_properties": {
+ "debug": true
+ }
+ }
+}
+```
-If you're not sure whether you need to install certain package or not, you can run the command `uv pip list` via subprocess (see the example below).
### Example: Listing preinstalled packages
@@ -29,10 +90,10 @@ The above code in the `config.json` file format for local testing:
```json
{
- "parameters": {
- "code": "import datetime\nimport subprocess\n\nprint(\"Hello world!\")\nprint(\"Current date and time:\", datetime.datetime.now())\nprint(\"See the full list of preinstalled packages:\")\n\nsubprocess.check_call([\"uv\", \"pip\", \"list\"])\n",
- "packages": []
- }
+ "parameters": {
+ "code": "import datetime\nimport subprocess\n\nprint(\"Hello world!\")\nprint(\"Current date and time:\", datetime.datetime.now())\nprint(\"See the full list of preinstalled packages:\")\n\nsubprocess.check_call([\"uv\", \"pip\", \"list\"])\n",
+ "packages": []
+ }
}
```
@@ -53,14 +114,14 @@ The above code in the `config.json` file format for local testing:
```json
{
- "parameters": {
- "code": "from keboola.component import CommonInterface\n\nci = CommonInterface()\n# access user parameters\nprint(ci.configuration.parameters)",
- "packages": [],
- "user_properties": {
- "debug": false
- "#secretCredentials": "theStrongestPasswordEver"
- }
+ "parameters": {
+ "code": "from keboola.component import CommonInterface\n\nci = CommonInterface()\n# access user parameters\nprint(ci.configuration.parameters)",
+ "packages": [],
+ "user_properties": {
+ "debug": false
+ "#secretCredentials": "theStrongestPasswordEver"
}
+ }
}
```
diff --git a/TEMPLATE_README.md b/TEMPLATE_README.md
deleted file mode 100644
index 8a62c2e..0000000
--- a/TEMPLATE_README.md
+++ /dev/null
@@ -1,223 +0,0 @@
-# KBC Component Python template
-
-Python template for KBC Component creation. Defines the default structure and all Bitbucket pipeline CI scripts for automatic deployment.
-
-Use as a starting point when creating a new component.
-
-Example uses [keboola.component](https://pypi.org/project/keboola.component) library providing useful methods for KBC related tasks
-and boilerplate methods often needed by components, for more details see [documentation](https://github.com/keboola/python-component/blob/main/README.md)
-
-*NOTE: Previously the template was based on top of the deprecated [keboola-python-util-lib library](https://bitbucket.org/kds_consulting_team/keboola-python-util-lib/src/master/)*
-
-**Table of contents:**
-
-[TOC]
-
-# Recommended component architecture
-It is recommended to use the [keboola.component library](https://pypi.org/project/keboola.component),
-for each component. Major advantage is that it reduces the boilerplate code replication, the developer can focus on core component logic
-and not on boilerplate tasks. If anything is missing in the library, please fork and create a pull request with additional changes,
-so we can all benefit from it
-
-**Base components on [CommonInterface](https://htmlpreview.github.io/?https://raw.githubusercontent.com/keboola/python-component/main/docs/api-html/component/interface.html#keboola.component.interface.CommonInterface)**
-
-- No need to write configuration processing and validation code each time
-- No need to setup logging environment manually
-- No need to write code to store manifests, write statefile, retrieve dates based on relative period, and many more.
-- The main focus can be the core component logic, which increases the code readability for new comers.
-
-**Base Client on [HtttpClient](https://pypi.org/project/keboola.http-client/)**
-
-- No need to write HTTP request handling over and over again
-- Covers basic authentication, retry strategy, headers, default parameters
-
-
-## Architecture using the template
-
-
-
-## Example component
-This template contains functional example of an [hello-world component](https://bitbucket.org/kds_consulting_team/kbc-python-template/src/master/src/component.py),
-it can be run with [sample configuration](https://bitbucket.org/kds_consulting_team/kbc-python-template/src/master/data/) and it produces valid results.
-It is advisable to use this structure as a base for new components. Especially the `component.py` module, which should only
-contain the base logic necessary for communication with KBC interface, processing parameters, collecting results
- and calling targeted API service methods.
-
-
-# Creating a new component
-Clone this repository into new folder and remove git history
-```bash
-git clone https://bitbucket.org/kds_consulting_team/kbc-python-template.git my-new-component
-cd my-new-component
-rm -rf .git
-git init
-git remote add origin PATH_TO_YOUR_BB_REPO
-git add .
-git commit -m 'initial'
-git push -u origin master
-```
-
-**Method #2:**
-
-Copy the contents of the template folder into your clone empty repository
-
-```bash
-git clone PATH_TO_YOUR_BB_REPO my-new-component
-# now copy the contents of the template into the my-new-component dir
-cd my-new-component
-git add .
-git commit -m 'initial'
-git push -u origin master
-```
-
-# Setting up the CI
- - Bitbucket: Enable [pipelines](https://confluence.atlassian.com/bitbucket/get-started-with-bitbucket-pipelines-792298921.html) in the repository.
- - For Github: Check that the [workflows are enabled](https://docs.github.com/en/actions/managing-workflow-runs/disabling-and-enabling-a-workflow).
- The actions are present in `.github/workflows/` folder.
- - Set `KBC_DEVELOPERPORTAL_APP` env variable (dev portal app id)
-
- In case it is not set on the account level, set also other required dev portal env variables:
-
- - `KBC_DEVELOPERPORTAL_PASSWORD` - service account password
- - `KBC_DEVELOPERPORTAL_USERNAME` - service account username
- - `KBC_DEVELOPERPORTAL_VENDOR` - dev portal vendor
- - `KBC_STORAGE_TOKEN` - (optional) in case you wish to run KBC automated tests
-
-
- 
-
-The script execution is defined in three stages:
-
-## Default stage
-This script is executed on push to any branch except the master branch. It executes basic build and code quality steps. Following steps are performed:
-Build docker image
-Execute flake8 lint tests
-Execute python unittest
-(Optional) Push image with tag :test into the AWS repository for manual testing in KBC
-If any of the above steps results in non 0 status, the build will fail. It is impossible to merge branches that fail to build into the master branch.
-
-## Master stage
-This script is executed on any push or change in the master branch. It performs every step as the default stage. Additionally,
-the `./scripts/update_dev_portal_properties.sh` script is executed.
-This script propagates all changes in the Component configuration files (component_config folder) to the Developer portal.
-Currently these Dev Portal configuration parameters are supported:
-
- - `configSchema.json`
- - `configRowSchema.json`
- - `component_short_description.md`
- - `component_long_description.md`
-
-The choice to include this script directly in the master branch was made to simplify ad-hoc changes of the component configuration parameters. For instance if you wish to slightly modify the configuration schema without affecting the code itself, it is possible to simply push the changes directly into the master and these will be automatically propagated to the production without rebuilding the image itself. Solely Developer Portal configuration metadata is deployed at this stage.
-
-## Tagged commit stage
-Whenever a tagged commit is added, or tag created this script gets executed. This is a deployment phase, so a successful build results in new code being deployed in KBC production.
-At this stage all steps present in the default and master stage are executed. Additionally,
-`deploy.sh` script that pushes the newly built image / tag into the ECR repository and KBC production is executed.
-The deploy script is executed only after all tests and proper build steps passed.
-Moreover, the `deploy.sh` script will be executed **only in the master branch**. In other words if you create a tagged commit in another branch, the pipeline gets triggered but deployment script will fail, because it is not triggered within a master branch. This is to prevent accidental deployment from a feature branch.
-
-
-# GELF logging
-
-The template automatically chooses between STDOUT and GELF logger based on the Developer Portal configuration.
-
-To fully leverage the benefits such as outputting the `Stack Trace` into the log event detail (available by clicking on the log event)
-log exceptions using `logger.exception(ex)`.
-
-**TIP:** When the logger verbosity is set to `verbose` you may leverage `extra` fields to log the detailed message in the detail of the log event by adding extra fields to you messages:
-
-```python
-logging.error(f'{error}. See log detail for full query. ',
- extra={"failed_query": json.dumps(query)})
-```
-
-Recommended [GELF logger setup](https://developers.keboola.com/extend/common-interface/logging/#setting-up) (Developer Portal) to allow debug mode logging:
-
-```json
-{
- "verbosity": {
- "100": "normal",
- "200": "normal",
- "250": "normal",
- "300": "verbose",
- "400": "verbose",
- "500": "camouflage",
- "550": "camouflage",
- "600": "camouflage"
- },
- "gelf_server_type": "tcp"
-}
-```
-
-# Development
-
-This example contains runnable container with simple unittest. For local testing it is useful to include `data` folder in the root
-and use docker-compose commands to run the container or execute tests.
-
-If required, change local data folder (the `CUSTOM_FOLDER` placeholder) path to your custom path:
-```yaml
- volumes:
- - ./:/code
- - ./CUSTOM_FOLDER:/data
-```
-
-Clone this repository, init the workspace and run the component with following command:
-
-```
-git clone https://bitbucket.org:kds_consulting_team/kbc-python-template.git my-new-component
-cd my-new-component
-docker-compose build
-docker-compose run --rm dev
-```
-
-Run the test suite and lint check using this command:
-
-```
-docker-compose run --rm test
-```
-
-## Testing
-
-The preset pipeline scripts contain sections allowing pushing testing image into the ECR repository and automatic
-testing in a dedicated project. These sections are by default commented out.
-
-**Running KBC tests on deploy step, before deployment**
-
-Uncomment following section in the deployment step in `bitbucket-pipelines.yml` file:
-
-```yaml
- # push test image to ECR - uncomment when initialised
- # - export REPOSITORY=`docker run --rm -e KBC_DEVELOPERPORTAL_USERNAME -e KBC_DEVELOPERPORTAL_PASSWORD -e KBC_DEVELOPERPORTAL_URL quay.io/keboola/developer-portal-cli-v2:latest ecr:get-repository $KBC_DEVELOPERPORTAL_VENDOR $KBC_DEVELOPERPORTAL_APP`
- # - docker tag $APP_IMAGE:latest $REPOSITORY:test
- # - eval $(docker run --rm -e KBC_DEVELOPERPORTAL_USERNAME -e KBC_DEVELOPERPORTAL_PASSWORD -e KBC_DEVELOPERPORTAL_URL quay.io/keboola/developer-portal-cli-v2:latest ecr:get-login $KBC_DEVELOPERPORTAL_VENDOR $KBC_DEVELOPERPORTAL_APP)
- # - docker push $REPOSITORY:test
- # - docker run --rm -e KBC_STORAGE_TOKEN quay.io/keboola/syrup-cli:latest run-job $KBC_DEVELOPERPORTAL_APP BASE_KBC_CONFIG test
- # - docker run --rm -e KBC_STORAGE_TOKEN quay.io/keboola/syrup-cli:latest run-job $KBC_DEVELOPERPORTAL_APP KBC_CONFIG_1 test
- - ./scripts/update_dev_portal_properties.sh
- - ./deploy.sh
-```
-
-Make sure that you have `KBC_STORAGE_TOKEN` env. variable set, containing appropriate storage token with access
-to your KBC project. Also make sure to create a functional testing configuration and replace the `BASE_KBC_CONFIG` placeholder with its id.
-
-**Pushing testing image for manual KBC tests**
-
-In some cases you may wish to execute a testing version of your component manually prior to publishing. For instance to test various
-configurations on it. For that it may be convenient to push the `test` image on every push either to master, or any branch.
-
-To achieve that simply uncomment appropriate sections in `bitbucket-pipelines.yml` file, either in master branch step or in `default` step.
-
-```yaml
- # push test image to ecr - uncomment for testing before deployment
-# - echo 'Pushing test image to repo. [tag=test]'
-# - export REPOSITORY=`docker run --rm -e KBC_DEVELOPERPORTAL_USERNAME -e KBC_DEVELOPERPORTAL_PASSWORD -e KBC_DEVELOPERPORTAL_URL quay.io/keboola/developer-portal-cli-v2:latest ecr:get-repository $KBC_DEVELOPERPORTAL_VENDOR $KBC_DEVELOPERPORTAL_APP`
-# - docker tag $APP_IMAGE:latest $REPOSITORY:test
-# - eval $(docker run --rm -e KBC_DEVELOPERPORTAL_USERNAME -e KBC_DEVELOPERPORTAL_PASSWORD -e KBC_DEVELOPERPORTAL_URL quay.io/keboola/developer-portal-cli-v2:latest ecr:get-login $KBC_DEVELOPERPORTAL_VENDOR $KBC_DEVELOPERPORTAL_APP)
-# - docker push $REPOSITORY:test
-```
-
- Once the build is finished, you may run such configuration in any KBC project as many times as you want by using [run-job](https://kebooladocker.docs.apiary.io/#reference/run/create-a-job-with-image/run-job) API call, using the `test` image tag.
-
-# Integration
-
-For information about deployment and integration with KBC, please refer to the [deployment section of developers documentation](https://developers.keboola.com/extend/component/deployment/)
\ No newline at end of file
diff --git a/component_config/configSchema.json b/component_config/configSchema.json
index 8c11e72..d6f6faa 100644
--- a/component_config/configSchema.json
+++ b/component_config/configSchema.json
@@ -7,51 +7,191 @@
"packages"
],
"properties": {
- "code": {
- "type": "string",
- "title": "Python code",
+ "user_properties": {
+ "type": "object",
+ "title": "User Parameters",
"format": "editor",
- "default": "from keboola.component import CommonInterface\n\nci = CommonInterface()\n# access user parameters\nprint(ci.configuration.parameters)",
+ "propertyOrder": 10,
+ "default": {
+ "debug": false
+ },
"options": {
+ "tooltip": "User parameters will be inserted in the `/data/config.json` file. They can be accessed in the code via `keboola.component.CommonInterface`, see an example in the documentation or when creating a new configuration.",
"editor": {
- "mode": "text/x-python",
+ "lint": true,
+ "mode": "application/json",
"lineNumbers": true,
"input_height": "100px"
}
+ }
+ },
+ "venv": {
+ "enum": [
+ "base",
+ "3.12",
+ "3.13",
+ "3.14"
+ ],
+ "type": "string",
+ "title": "Python Version & Environment Isolation",
+ "default": "3.13",
+ "options": {
+ "tooltip": "- Isolated environment takes a couple of seconds to start, but gives you the opportuninty to pick one of the latest versions of Python. It's also a safer choice as it prevents package collisions.\n- Non-isolated environment (used to be the default choice) might start a bit faster, but can lead to issues mentioned above.",
+ "enum_titles": [
+ "Python 3.10 – Shared environment (contains many pre-installed packages in legacy versions)",
+ "Python 3.12 – Isolated environment (just the packages of your choice)",
+ "Python 3.13 – Isolated environment (just the packages of your choice)",
+ "Python 3.14 beta – Isolated environment (just the packages of your choice)"
+ ]
+ },
+ "required": false,
+ "propertyOrder": 30
+ },
+ "source": {
+ "type": "radio",
+ "title": "Source Code & Dependencies",
+ "propertyOrder": 30,
+ "enum": [
+ "code",
+ "git"
+ ],
+ "options": {
+ "tooltip": "If you choose to provide the code to be ran via Git repository, any custom packages to be installed have to be specified:\n\n- in a **pyproject.toml** file accompanied with its corresponding **uv.lock file** (the modern way), or\n- in a **requirements.txt** file (the old way).\n\nThese files need to be present in the root folder of your repository. When all the aforementioned files are present, the modern way takes precedence.",
+ "enum_titles": [
+ "Enter manually into text areas below",
+ "Get from Git repository"
+ ]
},
- "propertyOrder": 10
+ "default": "code",
+ "required": false
},
"packages": {
"type": "array",
"items": {
"type": "string"
},
- "title": "Python packages",
+ "title": "Python Packages",
"format": "select",
+ "propertyOrder": 40,
"options": {
+ "dependencies": {
+ "source": "code"
+ },
"tags": true
},
"description": "Learn more about package installation, usage, and the list of pre-installed packages in our documentation.",
- "uniqueItems": true,
- "propertyOrder": 1
+ "uniqueItems": true
},
- "user_properties": {
- "type": "object",
- "title": "User Parameters",
+ "code": {
+ "type": "string",
+ "title": "Python Code",
"format": "editor",
- "default": {
- "debug": false
- },
+ "propertyOrder": 50,
+ "default": "from keboola.component import CommonInterface\n\nci = CommonInterface()\n# access user parameters\nprint(ci.configuration.parameters)",
"options": {
+ "dependencies": {
+ "source": "code"
+ },
"editor": {
- "lint": true,
- "mode": "application/json",
+ "mode": "text/x-python",
"lineNumbers": true,
"input_height": "100px"
}
+ }
+ },
+ "git": {
+ "type": "object",
+ "title": "Git Repository Source Settings",
+ "propertyOrder": 60,
+ "options": {
+ "dependencies": {
+ "source": "git"
+ }
},
- "description": "User parameters are accessible, and the result will be injected into the standard data/config.json parameters property, as in any other component.",
- "propertyOrder": 1
+ "required": [
+ "url",
+ "branch",
+ "filename"
+ ],
+ "properties": {
+ "url": {
+ "type": "string",
+ "title": "Repository URL",
+ "propertyOrder": 70
+ },
+ "branch": {
+ "type": "string",
+ "enum": [],
+ "title": "Branch Name",
+ "propertyOrder": 80,
+ "options": {
+ "async": {
+ "label": "List Branches",
+ "action": "listBranches",
+ "autoload": [
+ "git.url"
+ ],
+ "cache": false
+ }
+ }
+ },
+ "filename": {
+ "type": "string",
+ "enum": [],
+ "title": "Script Filename",
+ "propertyOrder": 90,
+ "options": {
+ "async": {
+ "label": "List Files",
+ "action": "listFiles",
+ "autoload": [
+ "git.branch"
+ ],
+ "cache": false
+ }
+ }
+ },
+ "auth": {
+ "type": "radio",
+ "title": "Repository Visibility & Authentication",
+ "propertyOrder": 100,
+ "enum": [
+ "none",
+ "pat",
+ "ssh"
+ ],
+ "options": {
+ "enum_titles": [
+ "Public – None",
+ "Private – Personal Access Token",
+ "Private – SSH Key"
+ ]
+ },
+ "default": "none",
+ "required": false
+ },
+ "#token": {
+ "type": "string",
+ "title": "Personal Access Token",
+ "propertyOrder": 110,
+ "options": {
+ "dependencies": {
+ "auth": "pat"
+ }
+ }
+ },
+ "ssh_keys": {
+ "type": "object",
+ "format": "ssh-editor",
+ "propertyOrder": 120,
+ "options": {
+ "only_keys": true,
+ "dependencies": {
+ "auth": "ssh"
+ }
+ }
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/component_config/sample-config/config.json b/component_config/sample-config/config.json
deleted file mode 100644
index abf4e98..0000000
--- a/component_config/sample-config/config.json
+++ /dev/null
@@ -1,53 +0,0 @@
-{
- "storage": {
- "input": {
- "files": [],
- "tables": [
- {
- "source": "in.c-test.test",
- "destination": "test.csv",
- "limit": 50,
- "columns": [],
- "where_values": [],
- "where_operator": "eq"
- }
- ]
- },
- "output": {
- "files": [],
- "tables": []
- }
- },
- "parameters": {
- "#api_token": "demo",
- "period_from": "yesterday",
- "endpoints": [
- "deals",
- "companies"
- ],
- "company_properties": "",
- "deal_properties": "",
- "debug": true
- },
- "image_parameters": {
- "syrup_url": "https://syrup.keboola.com/"
- },
- "authorization": {
- "oauth_api": {
- "id": "OAUTH_API_ID",
- "credentials": {
- "id": "main",
- "authorizedFor": "Myself",
- "creator": {
- "id": "1234",
- "description": "me@keboola.com"
- },
- "created": "2016-01-31 00:13:30",
- "#data": "{\"refresh_token\":\"MCWBkfdK9m5YK*Oqahwm6XN6elMAEwcH5kYcK8Ku!bpiOgSDZN9MQIzunpMsh6LyKH0i!7OcwwwajuxPfvm2PrrWYSs*HerDr2ZSJ39pqHJcvwUNIvHdtcgFFr3Em*yhn3GKBwM2p9UrjtgdAriSDny5YgUYGuI3gYJY1ypD*wBaAOzzeeXZx6CdgjruJ7gboTAngbWk3CzO9rORIwXAAlGUH6ZgBQJL3AwkYVMRFV4BvIvDAMF*0DcGDyrcyYDw9X3vYn*Wy!OqgrenKCGowdJk0C0136SUv4PJI383y76UMim6Q7KGDj7Lf!K2N2FDbxsz2iZKZTBr2vHx8pEC1oBc$\"}",
- "oauthVersion": "2.0",
- "appKey": "000000004C184A49",
- "#appSecret": "vBAYak49pVK1zghHAgDH4tCSCNlT-CiN"
- }
- }
- }
-}
diff --git a/component_config/sample-config/in/files/order1.xml b/component_config/sample-config/in/files/order1.xml
deleted file mode 100644
index 2567c87..0000000
--- a/component_config/sample-config/in/files/order1.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
- 1
- 2018-01-01
- David
-
- 100
- - Umbrella
-
-
- 200
- - Rain Coat
-
-
-
-
\ No newline at end of file
diff --git a/component_config/sample-config/in/state.json b/component_config/sample-config/in/state.json
deleted file mode 100644
index a0a9cd1..0000000
--- a/component_config/sample-config/in/state.json
+++ /dev/null
@@ -1 +0,0 @@
-{"data_delta": "10222018"}
\ No newline at end of file
diff --git a/component_config/sample-config/in/tables/test.csv b/component_config/sample-config/in/tables/test.csv
deleted file mode 100644
index 30eb73e..0000000
--- a/component_config/sample-config/in/tables/test.csv
+++ /dev/null
@@ -1,22 +0,0 @@
-"Type","Campaign_Name","Status","Start_Date","End_Date","Location","Eventbrite_link"
-"Event","How to become data driven startup","Complete","2015-10-13","2015-10-13","United Kingdom","https://www.eventbrite.co.uk/e/how-to-become-data-driven-startup-registration-18711425377"
-"Event","How to become data driven startup","Complete","2015-11-04","2015-11-04","United Kingdom","https://www.eventbrite.co.uk/e/how-to-become-data-driven-startup-registration-18711426380"
-"Event","How to become data driven startup","Complete","2015-10-13","2015-10-13","United Kingdom","https://www.eventbrite.co.uk/e/how-to-become-data-driven-startup-registration-18711425377"
-"Event","How to become data driven startup","Complete","2015-11-04","2015-11-04","United Kingdom","https://www.eventbrite.co.uk/e/how-to-become-data-driven-startup-registration-18711426380"
-"Event","DATAGIRLS PRESENT: HOW TO BECOME DATA-DRIVEN","Complete","2016-01-14","2016-01-14","United Kingdom","https://www.eventbrite.co.uk/e/datagirls-present-how-to-become-data-driven-tickets-20152992142"
-"Event","DATAGIRLS PRESENT: HOW TO BECOME DATA-DRIVEN","Complete","2016-02-25","2016-02-25","United Kingdom","https://www.eventbrite.co.uk/e/datagirls-present-how-to-become-data-driven-tickets-20967439175"
-"Event","Data Tools for Startups","Complete","2016-03-17","2016-03-17","United Kingdom","https://www.eventbrite.co.uk/e/data-tools-for-startups-tickets-21257426535"
-"Event","Data Festival London 2016","Complete","2016-06-24","2016-06-26","United Kingdom","https://www.eventbrite.co.uk/e/data-festival-london-2016-tickets-25192608771"
-"Event","Becoming data driven in the high street fashion","Complete","2016-10-12","2016-10-12","United Kingdom","https://www.eventbrite.co.uk/e/becoming-data-driven-in-the-high-street-fashion-tickets-27481268213"
-"Event","The Data Foundry present: DATAGIRLS Weekend","Complete","2016-10-14","2016-10-16","United Kingdom","https://www.eventbrite.co.uk/e/the-data-foundry-present-datagirls-weekend-tickets-27350069795"
-"Event","[NLP] How to analyse text data for knowledge discovery","Complete","2017-04-10","2017-04-10","United Kingdom","https://www.eventbrite.co.uk/e/nlp-how-to-analyse-text-data-for-knowledge-discovery-tickets-32320274812"
-"Event","Keboola DataBrunch - Amazon Go a ako s nĂm v maloobchode “bojovať”","Complete","2017-03-09","2017-03-09","Slovakia","https://www.eventbrite.co.uk/e/keboola-databrunch-amazon-go-a-ako-s-nim-v-maloobchode-bojovat-tickets-31827553068"
-"Event","Keboola DataBrunch - Amazon Go a jak s nim v maloobchodě “bojovat”","Complete","2017-03-29","2017-03-29","Czech Republic","https://www.eventbrite.co.uk/e/keboola-databrunch-amazon-go-a-jak-s-nim-v-maloobchode-bojovat-tickets-32182393405"
-"Event","The Data Foundry present: DATAGIRLS Weekend","Complete","2016-10-14","2016-10-16","United Kingdom","https://www.eventbrite.co.uk/e/the-data-foundry-present-datagirls-weekend-tickets-27350069795"
-"Event","[NLP] How to analyse text data for knowledge discovery","Complete","2017-04-10","2017-04-10","United Kingdom","https://www.eventbrite.co.uk/e/nlp-how-to-analyse-text-data-for-knowledge-discovery-tickets-32320274812"
-"Event","Keboola Data Brunch - KPIs and AmazonGo, budoucnost retailu? ","Complete","2017-06-27","2017-06-27","Czech Republic","https://www.eventbrite.co.uk/e/keboola-data-brunch-kpis-amazongo-budoucnost-retailu-tickets-35257195220"
-"Event","Learn how to #DoMoreWithData with DataGirls","Complete","2017-10-01","2017-10-01","United Kingdom","https://www.eventbrite.co.uk/e/learn-how-to-domorewithdata-with-datagirls-tickets-36777944823"
-"Event","Are You Using Data to Understand Your Customers? ","Complete","2018-02-27","2018-02-27","United Kingdom","https://www.eventbrite.co.uk/e/are-you-using-data-to-understand-your-customers-tickets-42000160611"
-"Event","Conversion Rate Optimisation in Travel Industry","Complete","2018-01-30","2018-01-30","United Kingdom","https://www.eventbrite.co.uk/e/conversion-rate-optimisation-in-travel-industry-tickets-38951076719"
-"Event","Learn how to #DoMoreWithData with DataGirls","Complete","2017-10-01","2017-10-01","United Kingdom","https://www.eventbrite.co.uk/e/learn-how-to-domorewithdata-with-datagirls-tickets-36777944823"
-"Event","Are You Using Data to Understand Your Customers? ","Complete","2018-02-27","2018-02-27","United Kingdom","https://www.eventbrite.co.uk/e/are-you-using-data-to-understand-your-customers-tickets-42000160611"
diff --git a/component_config/sample-config/in/tables/test.csv.manifest b/component_config/sample-config/in/tables/test.csv.manifest
deleted file mode 100644
index 5ea9f67..0000000
--- a/component_config/sample-config/in/tables/test.csv.manifest
+++ /dev/null
@@ -1,76 +0,0 @@
-{
- "id": "in.c-test.test",
- "uri": "https:\/\/connection.keboola.com\/v2\/storage\/tables\/in.c-test.test",
- "name": "test",
- "primary_key": [],
- "indexed_columns": [],
- "created": "2018-03-02T15:36:50+0100",
- "last_change_date": "2018-03-02T15:36:54+0100",
- "last_import_date": "2018-03-02T15:36:54+0100",
- "rows_count": 0,
- "data_size_bytes": 0,
- "is_alias": false,
- "attributes": [],
- "columns": [
- "Type",
- "Campaign_Name",
- "Status",
- "Start_Date",
- "End_Date",
- "Location",
- "Eventbrite_link"
- ],
- "metadata": [
- {
- "id": "18271581",
- "key": "KBC.createdBy.component.id",
- "value": "transformation",
- "provider": "system",
- "timestamp": "2018-03-02T15:37:02+0100"
- },
- {
- "id": "18271582",
- "key": "KBC.createdBy.configuration.id",
- "value": "361585608",
- "provider": "system",
- "timestamp": "2018-03-02T15:37:02+0100"
- },
- {
- "id": "18271583",
- "key": "KBC.createdBy.configurationRow.id",
- "value": "361585762",
- "provider": "system",
- "timestamp": "2018-03-02T15:37:02+0100"
- },
- {
- "id": "18271584",
- "key": "KBC.lastUpdatedBy.component.id",
- "value": "transformation",
- "provider": "system",
- "timestamp": "2018-03-02T15:37:02+0100"
- },
- {
- "id": "18271585",
- "key": "KBC.lastUpdatedBy.configuration.id",
- "value": "361585608",
- "provider": "system",
- "timestamp": "2018-03-02T15:37:02+0100"
- },
- {
- "id": "18271586",
- "key": "KBC.lastUpdatedBy.configurationRow.id",
- "value": "361585762",
- "provider": "system",
- "timestamp": "2018-03-02T15:37:02+0100"
- }
- ],
- "column_metadata": {
- "Type": [],
- "Campaign_Name": [],
- "Status": [],
- "Start_Date": [],
- "End_Date": [],
- "Location": [],
- "Eventbrite_link": []
- }
-}
\ No newline at end of file
diff --git a/component_config/sample-config/out/files/order1.xml b/component_config/sample-config/out/files/order1.xml
deleted file mode 100644
index 2567c87..0000000
--- a/component_config/sample-config/out/files/order1.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
- 1
- 2018-01-01
- David
-
- 100
- - Umbrella
-
-
- 200
- - Rain Coat
-
-
-
-
\ No newline at end of file
diff --git a/component_config/sample-config/out/tables/test.csv b/component_config/sample-config/out/tables/test.csv
deleted file mode 100644
index 30eb73e..0000000
--- a/component_config/sample-config/out/tables/test.csv
+++ /dev/null
@@ -1,22 +0,0 @@
-"Type","Campaign_Name","Status","Start_Date","End_Date","Location","Eventbrite_link"
-"Event","How to become data driven startup","Complete","2015-10-13","2015-10-13","United Kingdom","https://www.eventbrite.co.uk/e/how-to-become-data-driven-startup-registration-18711425377"
-"Event","How to become data driven startup","Complete","2015-11-04","2015-11-04","United Kingdom","https://www.eventbrite.co.uk/e/how-to-become-data-driven-startup-registration-18711426380"
-"Event","How to become data driven startup","Complete","2015-10-13","2015-10-13","United Kingdom","https://www.eventbrite.co.uk/e/how-to-become-data-driven-startup-registration-18711425377"
-"Event","How to become data driven startup","Complete","2015-11-04","2015-11-04","United Kingdom","https://www.eventbrite.co.uk/e/how-to-become-data-driven-startup-registration-18711426380"
-"Event","DATAGIRLS PRESENT: HOW TO BECOME DATA-DRIVEN","Complete","2016-01-14","2016-01-14","United Kingdom","https://www.eventbrite.co.uk/e/datagirls-present-how-to-become-data-driven-tickets-20152992142"
-"Event","DATAGIRLS PRESENT: HOW TO BECOME DATA-DRIVEN","Complete","2016-02-25","2016-02-25","United Kingdom","https://www.eventbrite.co.uk/e/datagirls-present-how-to-become-data-driven-tickets-20967439175"
-"Event","Data Tools for Startups","Complete","2016-03-17","2016-03-17","United Kingdom","https://www.eventbrite.co.uk/e/data-tools-for-startups-tickets-21257426535"
-"Event","Data Festival London 2016","Complete","2016-06-24","2016-06-26","United Kingdom","https://www.eventbrite.co.uk/e/data-festival-london-2016-tickets-25192608771"
-"Event","Becoming data driven in the high street fashion","Complete","2016-10-12","2016-10-12","United Kingdom","https://www.eventbrite.co.uk/e/becoming-data-driven-in-the-high-street-fashion-tickets-27481268213"
-"Event","The Data Foundry present: DATAGIRLS Weekend","Complete","2016-10-14","2016-10-16","United Kingdom","https://www.eventbrite.co.uk/e/the-data-foundry-present-datagirls-weekend-tickets-27350069795"
-"Event","[NLP] How to analyse text data for knowledge discovery","Complete","2017-04-10","2017-04-10","United Kingdom","https://www.eventbrite.co.uk/e/nlp-how-to-analyse-text-data-for-knowledge-discovery-tickets-32320274812"
-"Event","Keboola DataBrunch - Amazon Go a ako s nĂm v maloobchode “bojovať”","Complete","2017-03-09","2017-03-09","Slovakia","https://www.eventbrite.co.uk/e/keboola-databrunch-amazon-go-a-ako-s-nim-v-maloobchode-bojovat-tickets-31827553068"
-"Event","Keboola DataBrunch - Amazon Go a jak s nim v maloobchodě “bojovat”","Complete","2017-03-29","2017-03-29","Czech Republic","https://www.eventbrite.co.uk/e/keboola-databrunch-amazon-go-a-jak-s-nim-v-maloobchode-bojovat-tickets-32182393405"
-"Event","The Data Foundry present: DATAGIRLS Weekend","Complete","2016-10-14","2016-10-16","United Kingdom","https://www.eventbrite.co.uk/e/the-data-foundry-present-datagirls-weekend-tickets-27350069795"
-"Event","[NLP] How to analyse text data for knowledge discovery","Complete","2017-04-10","2017-04-10","United Kingdom","https://www.eventbrite.co.uk/e/nlp-how-to-analyse-text-data-for-knowledge-discovery-tickets-32320274812"
-"Event","Keboola Data Brunch - KPIs and AmazonGo, budoucnost retailu? ","Complete","2017-06-27","2017-06-27","Czech Republic","https://www.eventbrite.co.uk/e/keboola-data-brunch-kpis-amazongo-budoucnost-retailu-tickets-35257195220"
-"Event","Learn how to #DoMoreWithData with DataGirls","Complete","2017-10-01","2017-10-01","United Kingdom","https://www.eventbrite.co.uk/e/learn-how-to-domorewithdata-with-datagirls-tickets-36777944823"
-"Event","Are You Using Data to Understand Your Customers? ","Complete","2018-02-27","2018-02-27","United Kingdom","https://www.eventbrite.co.uk/e/are-you-using-data-to-understand-your-customers-tickets-42000160611"
-"Event","Conversion Rate Optimisation in Travel Industry","Complete","2018-01-30","2018-01-30","United Kingdom","https://www.eventbrite.co.uk/e/conversion-rate-optimisation-in-travel-industry-tickets-38951076719"
-"Event","Learn how to #DoMoreWithData with DataGirls","Complete","2017-10-01","2017-10-01","United Kingdom","https://www.eventbrite.co.uk/e/learn-how-to-domorewithdata-with-datagirls-tickets-36777944823"
-"Event","Are You Using Data to Understand Your Customers? ","Complete","2018-02-27","2018-02-27","United Kingdom","https://www.eventbrite.co.uk/e/are-you-using-data-to-understand-your-customers-tickets-42000160611"
diff --git a/docs/imgs/architecture.png b/docs/imgs/architecture.png
deleted file mode 100644
index b830dd2..0000000
Binary files a/docs/imgs/architecture.png and /dev/null differ
diff --git a/docs/imgs/ci_variable.png b/docs/imgs/ci_variable.png
deleted file mode 100644
index 3918d7b..0000000
Binary files a/docs/imgs/ci_variable.png and /dev/null differ
diff --git a/flake8.cfg b/flake8.cfg
index 4b7a645..de9858b 100644
--- a/flake8.cfg
+++ b/flake8.cfg
@@ -6,6 +6,7 @@ exclude =
tests,
example
venv
+ignore = E203,W503
max-line-length = 120
# F812: list comprehension redefines ...
diff --git a/pyproject.toml b/pyproject.toml
index 7bc67b6..3a997a5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,10 +1,10 @@
[project]
name = "custom-python"
-version = "0.1.0"
-description = "Add your description here"
+dynamic = ["version"]
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
+ "dacite>=1.9.2",
"keboola-component>=1.6.10",
]
diff --git a/scripts/build_n_run.ps1 b/scripts/build_n_run.ps1
deleted file mode 100644
index f279269..0000000
--- a/scripts/build_n_run.ps1
+++ /dev/null
@@ -1,27 +0,0 @@
-echo Building component...
-$COMP_TAG = Read-Host -Prompt 'Input Docker tag name:'
-docker build -rm -t $COMP_TAG ../
-
-echo Running component...
-Write-host "Would you like to use default data folder? (../data)" -ForegroundColor Yellow
- $Readhost = Read-Host " ( y / n ) "
- Switch ($ReadHost)
- {
- Y {Write-host "Yes use: " (join-path (Split-Path -Path (Get-Location).Path) "data"); $DATA_PATH = (join-path (Split-Path -Path (Get-Location).Path) "data") }
- N {Write-Host "No, I'll specify myself"; $DATA_PATH = Read-Host -Prompt 'Input data folder path:'}
- Default {Write-Host "Default, run app"; docker run -v $DATA_PATH`:/data -e KBC_DATADIR=/data $COMP_TAG}
- }
-
-Write-host "Would you like to execute the container to Bash, skipping the execution?" -ForegroundColor Yellow
- $Readhost = Read-Host " ( y / n ) "
- Switch ($ReadHost)
- {
- Y {Write-host "Yes, get me to the bash"; docker run -ti -v $DATA_PATH`:/data --entrypoint=//bin//bash $COMP_TAG}
- N {Write-Host "No, execute the app normally";
- echo $DATA_PATH
- docker run -v $DATA_PATH`:/data -e KBC_DATADIR=/data $COMP_TAG
- }
- Default {Write-Host "Default, run app"; docker run -v $DATA_PATH`:/data -e KBC_DATADIR=/data $COMP_TAG}
- }
-
-
diff --git a/scripts/build_n_test_docker.py b/scripts/build_n_test_docker.py
new file mode 100755
index 0000000..99bf4c4
--- /dev/null
+++ b/scripts/build_n_test_docker.py
@@ -0,0 +1,55 @@
+import logging
+import shutil
+import subprocess
+import sys
+import time
+from glob import glob
+
+logging.basicConfig(
+ level=logging.INFO,
+ format="[%(asctime)s - %(filename)s:%(lineno)d - %(levelname)s] %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+)
+
+
+if len(sys.argv) > 1:
+ data_dir = sys.argv[1]
+else:
+ data_dir = "./data"
+
+
+subprocess.run("docker build -t cp .", shell=True, check=True)
+
+all_passed = True
+results = []
+
+for i in range(1, 9):
+ config_files = glob(f"tests/config-{i}*.json")
+ if not config_files:
+ continue
+
+ filename = config_files[0]
+ logging.info(f"👉 Testing {filename}...")
+
+ shutil.copy(filename, f"{data_dir}/config.json")
+
+ start_time = time.time()
+ result = subprocess.run(["docker", "run", "-v", f"{data_dir}:/data", "-u", "1000:1000", "-it", "--rm", "cp:latest"])
+ elapsed_ms = round((time.time() - start_time) * 1000)
+
+ if result.returncode == 0:
+ results.append(f"âś… {filename}: PASSED ({elapsed_ms} ms)")
+ else:
+ all_passed = False
+ msg = f"❌ {filename}: FAILED ({elapsed_ms} ms)"
+ results.append(msg)
+ logging.info(msg)
+
+ shutil.rmtree("data/.venv", ignore_errors=True)
+
+logging.info("Results:\n" + "\n".join(f"- {r}" for r in results))
+
+if all_passed:
+ logging.info("✅ All tests passed! 🎉")
+else:
+ sys.exit(1)
diff --git a/scripts/run.bat b/scripts/run.bat
deleted file mode 100644
index 64da15d..0000000
--- a/scripts/run.bat
+++ /dev/null
@@ -1,4 +0,0 @@
-@echo off
-
-echo Running component...
-docker run -v %cd%:/data -e KBC_DATADIR=/data comp-tag
\ No newline at end of file
diff --git a/scripts/run_kbc_tests.ps1 b/scripts/run_kbc_tests.ps1
deleted file mode 100644
index 430e1a3..0000000
--- a/scripts/run_kbc_tests.ps1
+++ /dev/null
@@ -1,32 +0,0 @@
-echo "Preparing KBC test image"
-# set env vars
-$KBC_DEVELOPERPORTAL_USERNAME = Read-Host -Prompt 'Input your service account user name'
-$KBC_DEVELOPERPORTAL_PASSWORD = Read-Host -Prompt 'Input your service account pass'
-$KBC_DEVELOPERPORTAL_VENDOR = 'esnerda'
-$KBC_DEVELOPERPORTAL_APP = 'esnerda.ex-gusto-export'
-$BASE_KBC_CONFIG = '455568423'
-$KBC_STORAGE_TOKEN = Read-Host -Prompt 'Input your storage token'
-
-
-#build app
-$APP_IMAGE='keboola-comp-test'
-docker build ..\ --tag=$APP_IMAGE
-docker images
-docker -v
-#docker run $APP_IMAGE flake8 --config=./deployment/flake8.cfg
-echo "Running unit-tests..."
-docker run $APP_IMAGE python -m unittest discover
-
-docker pull quay.io/keboola/developer-portal-cli-v2:latest
-$REPOSITORY= docker run --rm -e KBC_DEVELOPERPORTAL_USERNAME=$KBC_DEVELOPERPORTAL_USERNAME -e KBC_DEVELOPERPORTAL_PASSWORD=$KBC_DEVELOPERPORTAL_PASSWORD quay.io/keboola/developer-portal-cli-v2:latest ecr:get-repository $KBC_DEVELOPERPORTAL_VENDOR $KBC_DEVELOPERPORTAL_APP
-
-docker tag $APP_IMAGE`:latest $REPOSITORY`:test
-
-echo 'running login'
-$(docker run --rm -e KBC_DEVELOPERPORTAL_USERNAME=$KBC_DEVELOPERPORTAL_USERNAME -e KBC_DEVELOPERPORTAL_PASSWORD=$KBC_DEVELOPERPORTAL_PASSWORD -e KBC_DEVELOPERPORTAL_URL quay.io/keboola/developer-portal-cli-v2:latest ecr:get-login $KBC_DEVELOPERPORTAL_VENDOR $KBC_DEVELOPERPORTAL_APP)
-
-echo 'pushing test image'
-docker push $REPOSITORY`:test
-
-echo 'running test config in KBC'
-docker run --rm -e KBC_STORAGE_TOKEN=$KBC_STORAGE_TOKEN quay.io/keboola/syrup-cli:latest run-job $KBC_DEVELOPERPORTAL_APP $BASE_KBC_CONFIG test
diff --git a/scripts/update_dev_portal_properties.sh b/scripts/update_dev_portal_properties.sh
deleted file mode 100644
index c1e4186..0000000
--- a/scripts/update_dev_portal_properties.sh
+++ /dev/null
@@ -1,111 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-# Obtain the component repository and log in
-docker pull quay.io/keboola/developer-portal-cli-v2:latest
-
-
-# Update properties in Keboola Developer Portal
-echo "Updating long description"
-value=`cat component_config/component_long_description.md`
-echo "$value"
-if [ ! -z "$value" ]
-then
- docker run --rm \
- -e KBC_DEVELOPERPORTAL_USERNAME \
- -e KBC_DEVELOPERPORTAL_PASSWORD \
- quay.io/keboola/developer-portal-cli-v2:latest \
- update-app-property ${KBC_DEVELOPERPORTAL_VENDOR} ${KBC_DEVELOPERPORTAL_APP} longDescription --value="$value"
-else
- echo "longDescription is empty!"
- exit 1
-fi
-
-echo "Updating config schema"
-value=`cat component_config/configSchema.json`
-echo "$value"
-if [ ! -z "$value" ]
-then
- docker run --rm \
- -e KBC_DEVELOPERPORTAL_USERNAME \
- -e KBC_DEVELOPERPORTAL_PASSWORD \
- quay.io/keboola/developer-portal-cli-v2:latest \
- update-app-property ${KBC_DEVELOPERPORTAL_VENDOR} ${KBC_DEVELOPERPORTAL_APP} configurationSchema --value="$value"
-else
- echo "configurationSchema is empty!"
-fi
-
-echo "Updating row config schema"
-value=`cat component_config/configRowSchema.json`
-echo "$value"
-if [ ! -z "$value" ]
-then
- docker run --rm \
- -e KBC_DEVELOPERPORTAL_USERNAME \
- -e KBC_DEVELOPERPORTAL_PASSWORD \
- quay.io/keboola/developer-portal-cli-v2:latest \
- update-app-property ${KBC_DEVELOPERPORTAL_VENDOR} ${KBC_DEVELOPERPORTAL_APP} configurationRowSchema --value="$value"
-else
- echo "configurationRowSchema is empty!"
-fi
-
-
-echo "Updating config description"
-
-value=`cat component_config/configuration_description.md`
-echo "$value"
-if [ ! -z "$value" ]
-then
- docker run --rm \
- -e KBC_DEVELOPERPORTAL_USERNAME \
- -e KBC_DEVELOPERPORTAL_PASSWORD \
- quay.io/keboola/developer-portal-cli-v2:latest \
- update-app-property ${KBC_DEVELOPERPORTAL_VENDOR} ${KBC_DEVELOPERPORTAL_APP} configurationDescription --value="$value"
-else
- echo "configurationDescription is empty!"
-fi
-
-
-echo "Updating short description"
-
-value=`cat component_config/component_short_description.md`
-echo "$value"
-if [ ! -z "$value" ]
-then
- docker run --rm \
- -e KBC_DEVELOPERPORTAL_USERNAME \
- -e KBC_DEVELOPERPORTAL_PASSWORD \
- quay.io/keboola/developer-portal-cli-v2:latest \
- update-app-property ${KBC_DEVELOPERPORTAL_VENDOR} ${KBC_DEVELOPERPORTAL_APP} shortDescription --value="$value"
-else
- echo "shortDescription is empty!"
-fi
-
-echo "Updating logger settings"
-
-value=`cat component_config/logger`
-echo "$value"
-if [ ! -z "$value" ]
-then
- docker run --rm \
- -e KBC_DEVELOPERPORTAL_USERNAME \
- -e KBC_DEVELOPERPORTAL_PASSWORD \
- quay.io/keboola/developer-portal-cli-v2:latest \
- update-app-property ${KBC_DEVELOPERPORTAL_VENDOR} ${KBC_DEVELOPERPORTAL_APP} logger --value="$value"
-else
- echo "logger type is empty!"
-fi
-
-echo "Updating logger configuration"
-value=`cat component_config/loggerConfiguration.json`
-echo "$value"
-if [ ! -z "$value" ]
-then
- docker run --rm \
- -e KBC_DEVELOPERPORTAL_USERNAME \
- -e KBC_DEVELOPERPORTAL_PASSWORD \
- quay.io/keboola/developer-portal-cli-v2:latest \
- update-app-property ${KBC_DEVELOPERPORTAL_VENDOR} ${KBC_DEVELOPERPORTAL_APP} loggerConfiguration --value="$value"
-else
- echo "loggerConfiguration is empty!"
-fi
\ No newline at end of file
diff --git a/src/component.py b/src/component.py
index 8727a07..d909096 100644
--- a/src/component.py
+++ b/src/component.py
@@ -5,23 +5,21 @@
import json
import logging
import os
-import runpy
-import subprocess
import sys
import traceback
+from pathlib import Path
from traceback import TracebackException
-from keboola.component.base import ComponentBase
+import dacite
+from keboola.component.base import ComponentBase, sync_action
from keboola.component.exceptions import UserException
-# configuration variables
-KEY_API_TOKEN = "#api_token"
-KEY_PRINT_HELLO = "print_hello"
-
-# list of mandatory parameters => if some is missing,
-# component will fail with readable message on initialization.
-REQUIRED_PARAMETERS = [KEY_PRINT_HELLO]
-REQUIRED_IMAGE_PARS = []
+from configuration import AuthEnum, Configuration, SourceEnum, VenvEnum, encrypted_keys
+from package_installer import PackageInstaller
+from source_file import FileHandler
+from source_git import GitHandler
+from subprocess_runner import SubprocessRunner
+from venv_manager import VenvManager
class Component(ComponentBase):
@@ -37,27 +35,44 @@ class Component(ComponentBase):
def __init__(self):
super().__init__()
+ self._set_init_logging_handler()
+ self.parameters = dacite.from_dict(
+ Configuration,
+ self.configuration.parameters,
+ config=dacite.Config(
+ cast=[AuthEnum, SourceEnum, VenvEnum],
+ convert_key=encrypted_keys,
+ ),
+ )
def run(self):
- parameters = self.configuration.parameters
+ if self.parameters.source == SourceEnum.CODE:
+ base_path = Path(self.data_folder_path)
+ script_filename = FileHandler.prepare_script_file(self.data_folder_path, self.parameters.code)
+ else:
+ base_path = Path(GitHandler.REPO_PATH).absolute()
+ git_handler = GitHandler(self.parameters.git)
+ script_filename = git_handler.clone_repository()
- self._set_init_logging_handler()
- script_path = os.path.join(self.data_folder_path, "script.py")
- self.prepare_script_file(script_path)
+ if self.parameters.venv == VenvEnum.BASE:
+ logging.info("Using base image environment")
+ else:
+ logging.info("Creating new Python %s virtual environment", self.parameters.venv.value)
+ venv_path = VenvManager.prepare_venv(self.parameters.venv.value, base_path)
+ logging.info("Virtual environment created at %s", venv_path)
+ os.environ["UV_PROJECT_ENVIRONMENT"] = str(venv_path)
+ os.environ["VIRTUAL_ENV"] = str(venv_path)
+
+ if self.parameters.source == SourceEnum.CODE:
+ PackageInstaller.install_packages(self.parameters.packages)
+ else:
+ PackageInstaller.install_packages_for_repository(base_path)
self._merge_user_parameters()
- # install packages
- self.install_packages(parameters.get("packages", []))
-
- self.execute_script_file(script_path)
-
- def prepare_script_file(self, destination_path: str):
- script = self.configuration.parameters["code"]
- with open(destination_path, "w+") as file:
- file.write(script)
+ self.execute_script_file(script_filename)
- def execute_script_file(self, file_path):
+ def execute_script_file(self, file_path: Path):
# Change current working directory so that relative paths work
os.chdir(self.data_folder_path)
sys.path.append(self.data_folder_path)
@@ -65,9 +80,9 @@ def execute_script_file(self, file_path):
try:
with open(file_path) as file:
script = file.read()
- logging.debug('Executing script "%s"', self.script_excerpt(script))
- runpy.run_path(file_path)
- logging.info("Script finished")
+ logging.info("Executing script:\n%s", self.script_excerpt(script))
+ args = ["uv", "run", str(file_path)]
+ SubprocessRunner.run(args, "Script executed successfully.", "Script execution failed.")
except Exception as err:
_, _, tb = sys.exc_info()
stack_len = len(traceback.extract_tb(tb)[4:])
@@ -85,32 +100,19 @@ def _get_stack_trace_records(etype, value, tb, limit=None, chain=True):
@staticmethod
def script_excerpt(script):
- if len(script) > 1000:
- return script[0:500] + "\n...\n" + script[-500]
+ if len(script) > 640:
+ return script[:256] + "\n...\n" + script[-256:]
else:
return script
- @staticmethod
- def install_packages(packages):
- for package in packages:
- logging.info("Installing package: %s...", package)
- args = [
- "uv",
- "add",
- package,
- ]
- process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- stdout, stderr = process.communicate()
- process.poll()
- logging.info("Installation finished: %s. Full log in detail.", package, extra={"full_message": stdout})
- if process.poll() != 0:
- raise UserException(f"Failed to install package: {package}. Log in event detail.", stderr)
- elif stderr:
- logging.warning(stderr)
-
def _set_init_logging_handler(self):
for h in logging.getLogger().handlers:
- h.setFormatter(logging.Formatter("[Non-script message]: %(message)s"))
+ h.setFormatter(
+ logging.Formatter(
+ fmt="[%(asctime)s - %(filename)s:%(lineno)d - %(levelname)s] %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+ )
+ )
def _merge_user_parameters(self):
"""
@@ -122,10 +124,28 @@ def _merge_user_parameters(self):
config_data = self.configuration.config_data.copy()
# build config data and overwrite for the user script
- config_data["parameters"] = self.configuration.parameters.get("user_properties", {})
- with open(os.path.join(self.data_folder_path, "config.json"), "w+") as inp:
+ config_data["parameters"] = self.parameters.user_properties
+ with open(Path(self.data_folder_path) / "config.json", "w+") as inp:
json.dump(config_data, inp)
+ @sync_action("listBranches")
+ def get_repository_branches(self):
+ """
+ Returns a list of branches in the git repository.
+ This method is used to populate the branches dropdown in the UI.
+ """
+ git_handler = GitHandler(self.parameters.git)
+ return git_handler.get_repository_branches()
+
+ @sync_action("listFiles")
+ def get_repository_files(self):
+ """
+ Returns a list of branches in the git repository.
+ This method is used to populate the branches dropdown in the UI.
+ """
+ git_handler = GitHandler(self.parameters.git)
+ return git_handler.get_repository_files()
+
"""
Main entrypoint
diff --git a/src/configuration.py b/src/configuration.py
new file mode 100644
index 0000000..8c24cac
--- /dev/null
+++ b/src/configuration.py
@@ -0,0 +1,57 @@
+from dataclasses import dataclass, field
+from enum import Enum
+
+
+# the encrypted keys (prefixed with # in Keboola) have to be prefixed with "encrypted_" here
+def encrypted_keys(key: str) -> str:
+ return key.replace("encrypted_", "#") if key.startswith("encrypted_") else key
+
+
+class SourceEnum(Enum):
+ CODE = "code"
+ GIT = "git"
+
+
+class VenvEnum(Enum):
+ BASE = "base"
+ PY_3_12 = "3.12"
+ PY_3_13 = "3.13"
+ PY_3_14 = "3.14"
+
+
+class AuthEnum(Enum):
+ NONE = "none"
+ PAT = "pat"
+ SSH = "ssh"
+
+
+# the ssh_keys.keys.[#private,public] structure is based on Keboola's standard SSH keys UI element output structure
+@dataclass
+class KeysConfiguration:
+ public: str | None = None
+ encrypted_private: str | None = None
+
+
+@dataclass
+class SSHKeysConfiguration:
+ keys: KeysConfiguration = field(default_factory=KeysConfiguration)
+
+
+@dataclass
+class GitConfiguration:
+ url: str = ""
+ branch: str = "main"
+ filename: str = "main.py"
+ auth: AuthEnum = AuthEnum.NONE
+ encrypted_token: str | None = None
+ ssh_keys: SSHKeysConfiguration = field(default_factory=SSHKeysConfiguration)
+
+
+@dataclass
+class Configuration:
+ source: SourceEnum = SourceEnum.CODE
+ user_properties: dict[str, object] = field(default_factory=dict)
+ venv: VenvEnum = VenvEnum.BASE
+ packages: list[str] = field(default_factory=list)
+ code: str = ""
+ git: GitConfiguration = field(default_factory=GitConfiguration)
diff --git a/src/package_installer.py b/src/package_installer.py
new file mode 100644
index 0000000..962ecaf
--- /dev/null
+++ b/src/package_installer.py
@@ -0,0 +1,54 @@
+import logging
+import os
+from pathlib import Path
+
+from subprocess_runner import SubprocessRunner
+
+MSG_OK = "Installation successful."
+MSG_ERR = "Installation failed."
+
+
+class PackageInstaller:
+ @staticmethod
+ def install_packages(packages: list[str], use_pip=False):
+ if use_pip:
+ uv_args = ["pip", "install"]
+ else:
+ uv_args = ["add"]
+
+ for package in packages:
+ logging.info("Installing package: %s...", package)
+ args = ["uv", *uv_args, package]
+ SubprocessRunner.run(args, MSG_OK, MSG_ERR)
+
+ @staticmethod
+ def install_packages_for_repository(repository_path: Path):
+ """
+ Install packages based on the given repository path.
+ - If there is a pyproject.toml and a uv.lock file, run uv sync.
+ - If there is a requirements.txt file, install packages from it using uv.
+
+ Args:
+ repository_path (str): Path to the repository containing requirements.txt.
+ """
+ pyproject_file = repository_path / "pyproject.toml"
+ uv_lock_file = repository_path / "uv.lock"
+ requirements_file = repository_path / "requirements.txt"
+
+ # Explicitly install keboola.component in case user didn't include in their dependencies file
+ PackageInstaller.install_packages(["keboola.component"], use_pip=True)
+
+ args = None
+ if pyproject_file.exists() and uv_lock_file.exists():
+ logging.info("Running uv sync...")
+ os.chdir(repository_path) # it is currently impossible to pass custom uv.lock path
+ args = ["uv", "sync", "--inexact"]
+ elif requirements_file.exists():
+ logging.info("Installing packages from requirements.txt...")
+ args = ["uv", "pip", "install", "-r", str(requirements_file)]
+
+ if not args:
+ logging.info("No dependencies file found")
+ return
+
+ SubprocessRunner.run(args, MSG_OK, MSG_ERR)
diff --git a/src/source_file.py b/src/source_file.py
new file mode 100644
index 0000000..2222826
--- /dev/null
+++ b/src/source_file.py
@@ -0,0 +1,12 @@
+from pathlib import Path
+
+
+class FileHandler:
+ @staticmethod
+ def prepare_script_file(destination_path: str, script: str) -> Path:
+ script_filename = Path(destination_path) / "script.py"
+
+ with open(script_filename, "w") as file:
+ file.write(script)
+
+ return script_filename
diff --git a/src/source_git.py b/src/source_git.py
new file mode 100644
index 0000000..f60624f
--- /dev/null
+++ b/src/source_git.py
@@ -0,0 +1,173 @@
+import logging
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+from keboola.component.exceptions import UserException
+
+from configuration import AuthEnum, GitConfiguration
+
+
+class GitHandler:
+ REPO_PATH = "repo_clone"
+
+ def __init__(self, git_cfg: GitConfiguration):
+ # add path for absolute imports to start at the cloned repository root level
+ sys.path.append(str(Path(__file__).parent.parent / GitHandler.REPO_PATH))
+
+ self.env = os.environ.copy()
+ self.git_cfg = git_cfg
+ self.repo_auth_url = None # ‼️ NEVER EVER INCLUDE THIS VARIABLE IN LOGGING OUTPUT ‼️
+
+ if not self.git_cfg.url:
+ raise UserException("Git repository URL is required")
+
+ if self.git_cfg.auth == AuthEnum.PAT:
+ self._set_up_token_auth()
+
+ repo_url = self.git_cfg.url
+ if repo_url.startswith("git@") or repo_url.startswith("ssh://"):
+ self._set_up_ssh_command()
+
+ # do not ask for credentials when git authentication fails
+ self.env["GIT_TERMINAL_PROMPT"] = "0"
+
+ def _set_up_token_auth(self) -> None:
+ if not self.git_cfg.encrypted_token:
+ raise UserException("No personal access token provided")
+
+ if not self.git_cfg.url.startswith("https://"):
+ raise UserException("PAT authentication is only supported for HTTPS URLs")
+
+ self.repo_auth_url = self.git_cfg.url.replace(
+ "https://", f"https://x-token-auth:{self.git_cfg.encrypted_token}@"
+ )
+ logging.info("Git token authentication set up for HTTPS URL.")
+
+ def _set_up_ssh_command(self) -> None:
+ if not self.git_cfg.ssh_keys.keys.encrypted_private:
+ if self.git_cfg.auth == AuthEnum.SSH:
+ raise UserException("SSH key is required for SSH authentication")
+ elif self.git_cfg.auth == AuthEnum.NONE:
+ logging.warning("SSH URL detected but no SSH private key provided. Trying default SSH configuration.")
+
+ ssh_command = [
+ "ssh",
+ # the following lines could be used to disable strict host key checking, but it is better
+ # for security reasons to use the known_hosts file prepared in Dockerfile
+ # "-o",
+ # "StrictHostKeyChecking=no",
+ "-o",
+ "BatchMode=yes", # do not ask for credentials when SSH auth fails
+ "-o",
+ "ConnectTimeout=30",
+ "-o",
+ "ServerAliveInterval=60",
+ ]
+
+ if self.git_cfg.ssh_keys.keys.encrypted_private:
+ ssh_key_path = Path("~/.ssh/github_private_key").expanduser()
+ with open(ssh_key_path, "wb") as f:
+ for line in self.git_cfg.ssh_keys.keys.encrypted_private.splitlines():
+ f.write(line.encode() + b"\n")
+ # ensure SSH key has correct permissions
+ os.chmod(ssh_key_path, 0o600)
+ ssh_command.extend(["-i", str(ssh_key_path)])
+
+ self.env["GIT_SSH_COMMAND"] = " ".join(ssh_command)
+
+ def clone_repository(self, sync_action=False) -> Path:
+ """
+ Clone a git repository and return the path to the cloned code.
+
+ Returns:
+ Path to the main script file to execute
+ """
+
+ branch = self.git_cfg.branch or "main"
+ logging.info("Cloning git repository: %s", self.git_cfg.url)
+
+ try:
+ clone_args = ["git", "clone"]
+
+ if branch:
+ clone_args.extend(["--branch", branch])
+
+ clone_args.extend([self.repo_auth_url or self.git_cfg.url, GitHandler.REPO_PATH])
+
+ process = subprocess.Popen(
+ clone_args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=self.env,
+ )
+ _, stderr = process.communicate()
+
+ if process.returncode != 0:
+ error_msg = stderr.decode() if stderr else "Unknown git clone error"
+ if "Permission denied" in error_msg or "publickey" in error_msg:
+ error_msg += ". Please check SSH key configuration or use HTTPS URL."
+ raise UserException(f"Failed to clone git repository: {error_msg}")
+
+ logging.info("Successfully cloned repository")
+
+ # when cloning for the "list files" sync action, checking for the script file presence doesn't make sense
+ # and could cause problems in cases the repository changed for any reason
+ if sync_action:
+ return Path()
+
+ source_dir = Path.cwd() / GitHandler.REPO_PATH
+ main_script_path = Path(source_dir) / self.git_cfg.filename
+ if not main_script_path.is_file():
+ raise UserException(f"Main script file '{self.git_cfg.filename}' not found in repository")
+
+ return main_script_path
+
+ except Exception as e:
+ raise UserException(f"Error processing git repository: {str(e)}") from e
+
+ def get_repository_branches(self):
+ """
+ Get a list of branches in the git repository.
+
+ Returns:
+ List of branch names
+ """
+ try:
+ branches_args = ["git", "ls-remote", "--heads"]
+
+ branches_args.append(self.repo_auth_url or self.git_cfg.url)
+
+ process = subprocess.Popen(
+ branches_args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=self.env,
+ )
+ stdout, stderr = process.communicate()
+
+ if process.returncode != 0:
+ raise UserException(f"Failed to get branches: {stderr.decode()}")
+
+ branches = [line.strip().split("refs/heads/")[-1] for line in stdout.decode().splitlines() if line.strip()]
+ return [{"value": b, "label": b} for b in branches]
+
+ except Exception as e:
+ raise UserException(f"Error getting repository branches: {str(e)}") from e
+
+ def get_repository_files(self):
+ _ = self.clone_repository(sync_action=True)
+
+ files = []
+ for dirpath, _, filenames in os.walk(GitHandler.REPO_PATH):
+ if dirpath.startswith(f"{GitHandler.REPO_PATH}/.git"):
+ continue
+ for filename in filenames:
+ if not filename.endswith(".py"):
+ continue
+ path = str(Path(dirpath) / filename)
+ # strip the repository path prefix
+ files.append(path[len(GitHandler.REPO_PATH) + 1 :])
+
+ return [{"value": f, "label": f} for f in files]
diff --git a/src/subprocess_runner.py b/src/subprocess_runner.py
new file mode 100644
index 0000000..94c8b27
--- /dev/null
+++ b/src/subprocess_runner.py
@@ -0,0 +1,33 @@
+import logging
+import subprocess
+
+from keboola.component.exceptions import UserException
+
+
+class SubprocessRunner:
+ @staticmethod
+ def run(
+ args: list[str],
+ ok_message: str = "Command finished sucessfully.",
+ err_message: str = "Command failed.",
+ ):
+ logging.debug("Running command: %s", " ".join(args))
+ process = subprocess.Popen(
+ args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=False,
+ )
+
+ stdout, stderr = process.communicate()
+ if stdout:
+ logging.info("Command output:\n%s", stdout.decode())
+
+ process.poll()
+ if process.poll() != 0:
+ stderr_str = stderr.decode() if stderr else "Unknown error."
+ raise UserException(f"{err_message} Log in event detail.", stderr_str)
+ elif stderr:
+ logging.info("%s Full log in detail.", ok_message, extra={"full_message": stderr.decode()})
+ else:
+ logging.info(ok_message)
diff --git a/src/venv_manager.py b/src/venv_manager.py
new file mode 100644
index 0000000..9732990
--- /dev/null
+++ b/src/venv_manager.py
@@ -0,0 +1,21 @@
+from pathlib import Path
+
+from subprocess_runner import SubprocessRunner
+
+
+class VenvManager:
+ @staticmethod
+ def prepare_venv(py_version: str, base_path: Path) -> Path:
+ """
+ Prepare venv for the main script file given. The venv is always created in the same directory
+ as the main script file.
+
+ Args:
+ main_script_file (str): Path to the main script file.
+ """
+ venv_path = base_path / ".venv"
+ args = ["uv", "venv", "-p", py_version, str(venv_path)]
+
+ SubprocessRunner.run(args, "Environment created successfully.", "Environment creation failed.")
+
+ return venv_path
diff --git a/tests/config-1_git-example-1-base.json b/tests/config-1_git-example-1-base.json
new file mode 100644
index 0000000..db82660
--- /dev/null
+++ b/tests/config-1_git-example-1-base.json
@@ -0,0 +1,18 @@
+{
+ "parameters": {
+ "source": "git",
+ "git": {
+ "url": "https://github.com/keboola/component-custom-python-example-repo-1.git",
+ "auth": "none",
+ "branch": "main",
+ "filename": "main.py"
+ },
+ "user_properties": {
+ "debug": true,
+ "rectangle_a": 3.0,
+ "rectangle_b": 4.0,
+ "endpoint": "https://www.example.com"
+ },
+ "venv": "base"
+ }
+}
diff --git a/tests/config-2_git-example-1-3.13.json b/tests/config-2_git-example-1-3.13.json
new file mode 100644
index 0000000..24c6a12
--- /dev/null
+++ b/tests/config-2_git-example-1-3.13.json
@@ -0,0 +1,18 @@
+{
+ "parameters": {
+ "source": "git",
+ "git": {
+ "url": "https://github.com/keboola/component-custom-python-example-repo-1.git",
+ "auth": "none",
+ "branch": "main",
+ "filename": "main.py"
+ },
+ "user_properties": {
+ "debug": true,
+ "rectangle_a": 3.0,
+ "rectangle_b": 4.0,
+ "endpoint": "https://www.example.com"
+ },
+ "venv": "3.13"
+ }
+}
diff --git a/tests/config-3_git-example-2-base.json b/tests/config-3_git-example-2-base.json
new file mode 100644
index 0000000..214aae6
--- /dev/null
+++ b/tests/config-3_git-example-2-base.json
@@ -0,0 +1,18 @@
+{
+ "parameters": {
+ "source": "git",
+ "git": {
+ "url": "https://github.com/keboola/component-custom-python-example-repo-2.git",
+ "auth": "none",
+ "branch": "main",
+ "filename": "src/main.py"
+ },
+ "user_properties": {
+ "debug": true,
+ "rectangle_a": 3.0,
+ "rectangle_b": 4.0,
+ "endpoint": "https://www.example.com"
+ },
+ "venv": "base"
+ }
+}
diff --git a/tests/config-4_git-example-2-3.13.json b/tests/config-4_git-example-2-3.13.json
new file mode 100644
index 0000000..1e66782
--- /dev/null
+++ b/tests/config-4_git-example-2-3.13.json
@@ -0,0 +1,18 @@
+{
+ "parameters": {
+ "source": "git",
+ "git": {
+ "url": "https://github.com/keboola/component-custom-python-example-repo-2.git",
+ "auth": "none",
+ "branch": "main",
+ "filename": "src/main.py"
+ },
+ "user_properties": {
+ "debug": true,
+ "rectangle_a": 3.0,
+ "rectangle_b": 4.0,
+ "endpoint": "https://www.example.com"
+ },
+ "venv": "3.13"
+ }
+}
diff --git a/tests/config-5_code-base-pandas.json b/tests/config-5_code-base-pandas.json
new file mode 100644
index 0000000..58b67a4
--- /dev/null
+++ b/tests/config-5_code-base-pandas.json
@@ -0,0 +1,17 @@
+{
+ "parameters": {
+ "source": "code",
+ "packages": [
+ "httpx",
+ "pandas"
+ ],
+ "user_properties": {
+ "debug": true,
+ "rectangle_a": 3.0,
+ "rectangle_b": 4.0,
+ "endpoint": "https://www.example.com"
+ },
+ "venv": "base",
+ "code": "import sys\n\nprint(sys.executable, sys.version)\n\nimport httpx\nimport pandas\nfrom keboola.component import CommonInterface\n\n\nci = CommonInterface()\nprint(ci.configuration.parameters)\n\nurl = \"https://api.nationalize.io/?name=john\"\n\n\ndef get_json_from_url(url):\n try:\n response = httpx.get(url)\n response.raise_for_status()\n return response.json()\n except httpx.HTTPError as e:\n print(f\"Error fetching data from {url}: {e}\")\n return None\n\n\ndata = get_json_from_url(url)\nif data:\n print(data)\n"
+ }
+}
diff --git a/tests/config-6_code-3.13-pandas.json b/tests/config-6_code-3.13-pandas.json
new file mode 100644
index 0000000..49e153d
--- /dev/null
+++ b/tests/config-6_code-3.13-pandas.json
@@ -0,0 +1,17 @@
+{
+ "parameters": {
+ "source": "code",
+ "packages": [
+ "httpx",
+ "pandas"
+ ],
+ "user_properties": {
+ "debug": true,
+ "rectangle_a": 3.0,
+ "rectangle_b": 4.0,
+ "endpoint": "https://www.example.com"
+ },
+ "venv": "3.13",
+ "code": "import sys\n\nprint(sys.executable, sys.version)\n\nimport httpx\nimport pandas\nfrom keboola.component import CommonInterface\n\n\nci = CommonInterface()\nprint(ci.configuration.parameters)\n\nurl = \"https://api.nationalize.io/?name=john\"\n\n\ndef get_json_from_url(url):\n try:\n response = httpx.get(url)\n response.raise_for_status()\n return response.json()\n except httpx.HTTPError as e:\n print(f\"Error fetching data from {url}: {e}\")\n return None\n\n\ndata = get_json_from_url(url)\nif data:\n print(data)\n"
+ }
+}
\ No newline at end of file
diff --git a/tests/config-7_code-base-httpx-only.json b/tests/config-7_code-base-httpx-only.json
new file mode 100644
index 0000000..d931e5f
--- /dev/null
+++ b/tests/config-7_code-base-httpx-only.json
@@ -0,0 +1,16 @@
+{
+ "parameters": {
+ "source": "code",
+ "packages": [
+ "httpx"
+ ],
+ "user_properties": {
+ "debug": true,
+ "rectangle_a": 3.0,
+ "rectangle_b": 4.0,
+ "endpoint": "https://www.example.com"
+ },
+ "venv": "base",
+ "code": "import sys\n\nprint(sys.executable, sys.version)\n\nimport httpx\nfrom keboola.component import CommonInterface\n\n\nci = CommonInterface()\nprint(ci.configuration.parameters)\n\nurl = \"https://api.nationalize.io/?name=john\"\n\n\ndef get_json_from_url(url):\n try:\n response = httpx.get(url)\n response.raise_for_status()\n return response.json()\n except httpx.HTTPError as e:\n print(f\"Error fetching data from {url}: {e}\")\n return None\n\n\ndata = get_json_from_url(url)\nif data:\n print(data)\n"
+ }
+}
\ No newline at end of file
diff --git a/tests/config-8_code-3.13-httpx-only.json b/tests/config-8_code-3.13-httpx-only.json
new file mode 100644
index 0000000..4c2d819
--- /dev/null
+++ b/tests/config-8_code-3.13-httpx-only.json
@@ -0,0 +1,16 @@
+{
+ "parameters": {
+ "source": "code",
+ "packages": [
+ "httpx"
+ ],
+ "user_properties": {
+ "debug": true,
+ "rectangle_a": 3.0,
+ "rectangle_b": 4.0,
+ "endpoint": "https://www.example.com"
+ },
+ "venv": "3.13",
+ "code": "import sys\n\nprint(sys.executable, sys.version)\n\nimport httpx\nfrom keboola.component import CommonInterface\n\n\nci = CommonInterface()\nprint(ci.configuration.parameters)\n\nurl = \"https://api.nationalize.io/?name=john\"\n\n\ndef get_json_from_url(url):\n try:\n response = httpx.get(url)\n response.raise_for_status()\n return response.json()\n except httpx.HTTPError as e:\n print(f\"Error fetching data from {url}: {e}\")\n return None\n\n\ndata = get_json_from_url(url)\nif data:\n print(data)\n"
+ }
+}
\ No newline at end of file
diff --git a/tests/test_component.py b/tests/test_component.py
index 0962f37..d14c92b 100644
--- a/tests/test_component.py
+++ b/tests/test_component.py
@@ -1,11 +1,7 @@
-'''
-Created on 12. 11. 2018
-
-@author: esner
-'''
+import os
import unittest
+
import mock
-import os
from freezegun import freeze_time
from component import Component
diff --git a/uv.lock b/uv.lock
index 1c20df0..4aaeb9a 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,16 +1,13 @@
version = 1
revision = 2
requires-python = ">=3.10"
-resolution-markers = [
- "python_full_version >= '3.13'",
- "python_full_version < '3.13'",
-]
[[package]]
name = "custom-python"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
+ { name = "dacite" },
{ name = "keboola-component" },
]
@@ -22,7 +19,10 @@ dev = [
]
[package.metadata]
-requires-dist = [{ name = "keboola-component", specifier = ">=1.6.10" }]
+requires-dist = [
+ { name = "dacite", specifier = ">=1.9.2" },
+ { name = "keboola-component", specifier = ">=1.6.10" },
+]
[package.metadata.requires-dev]
dev = [
@@ -31,6 +31,15 @@ dev = [
{ name = "mock", specifier = ">=5.2.0" },
]
+[[package]]
+name = "dacite"
+version = "1.9.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/55/a0/7ca79796e799a3e782045d29bf052b5cde7439a2bbb17f15ff44f7aacc63/dacite-1.9.2.tar.gz", hash = "sha256:6ccc3b299727c7aa17582f0021f6ae14d5de47c7227932c47fec4cdfefd26f09", size = 22420, upload-time = "2025-02-05T09:27:29.757Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/35/386550fd60316d1e37eccdda609b074113298f23cef5bddb2049823fe666/dacite-1.9.2-py3-none-any.whl", hash = "sha256:053f7c3f5128ca2e9aceb66892b1a3c8936d02c686e707bee96e19deef4bc4a0", size = 16600, upload-time = "2025-02-05T09:27:24.345Z" },
+]
+
[[package]]
name = "deprecated"
version = "1.2.18"
@@ -38,35 +47,35 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload_time = "2025-01-27T10:46:25.7Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload_time = "2025-01-27T10:46:09.186Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" },
]
[[package]]
name = "flake8"
-version = "7.1.2"
+version = "7.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mccabe" },
{ name = "pycodestyle" },
{ name = "pyflakes" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/58/16/3f2a0bb700ad65ac9663262905a025917c020a3f92f014d2ba8964b4602c/flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd", size = 48119, upload_time = "2025-02-16T18:45:44.296Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/35/f8/08d37b2cd89da306e3520bd27f8a85692122b42b56c0c2c3784ff09c022f/flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", size = 57745, upload_time = "2025-02-16T18:45:42.351Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" },
]
[[package]]
name = "freezegun"
-version = "1.5.1"
+version = "1.5.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/2c/ef/722b8d71ddf4d48f25f6d78aa2533d505bf3eec000a7cacb8ccc8de61f2f/freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9", size = 33697, upload_time = "2024-05-11T17:32:53.911Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/75/0455fa5029507a2150da59db4f165fbc458ff8bb1c4f4d7e8037a14ad421/freezegun-1.5.2.tar.gz", hash = "sha256:a54ae1d2f9c02dbf42e02c18a3ab95ab4295818b549a34dac55592d72a905181", size = 34855, upload-time = "2025-05-24T12:38:47.051Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/51/0b/0d7fee5919bccc1fdc1c2a7528b98f65c6f69b223a3fd8f809918c142c36/freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1", size = 17569, upload_time = "2024-05-11T17:32:51.715Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/b2/68d4c9b6431121b6b6aa5e04a153cac41dcacc79600ed6e2e7c3382156f5/freezegun-1.5.2-py3-none-any.whl", hash = "sha256:5aaf3ba229cda57afab5bd311f0108d86b6fb119ae89d2cd9c43ec8c1733c85b", size = 18715, upload-time = "2025-05-24T12:38:45.274Z" },
]
[[package]]
@@ -78,54 +87,54 @@ dependencies = [
{ name = "pygelf" },
{ name = "pytz" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ef/dd/391f1e6eaae5e925f56f30788c40e38c8bce3a80ee898512e79ced2eabab/keboola.component-1.6.10.tar.gz", hash = "sha256:5f4c347e8e96bb4dff1fe1254217e88754a4edea8a1b147815552335dcfba941", size = 47336, upload_time = "2025-01-17T11:56:05.419Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ef/dd/391f1e6eaae5e925f56f30788c40e38c8bce3a80ee898512e79ced2eabab/keboola.component-1.6.10.tar.gz", hash = "sha256:5f4c347e8e96bb4dff1fe1254217e88754a4edea8a1b147815552335dcfba941", size = 47336, upload-time = "2025-01-17T11:56:05.419Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4c/34/301082c106bff256e2871a5c5db54d691fdad77a9d8a89a52eef30cd18ed/keboola.component-1.6.10-py3-none-any.whl", hash = "sha256:9a13b73beb71373d9a2b456eb44f902cfcfc07747c084bfbfad761b5eaaa4d93", size = 42243, upload_time = "2025-01-17T11:56:04.215Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/34/301082c106bff256e2871a5c5db54d691fdad77a9d8a89a52eef30cd18ed/keboola.component-1.6.10-py3-none-any.whl", hash = "sha256:9a13b73beb71373d9a2b456eb44f902cfcfc07747c084bfbfad761b5eaaa4d93", size = 42243, upload-time = "2025-01-17T11:56:04.215Z" },
]
[[package]]
name = "mccabe"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload_time = "2022-01-24T01:14:51.113Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload_time = "2022-01-24T01:14:49.62Z" },
+ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
]
[[package]]
name = "mock"
version = "5.2.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/07/8c/14c2ae915e5f9dca5a22edd68b35be94400719ccfa068a03e0fb63d0f6f6/mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0", size = 92796, upload_time = "2025-03-03T12:31:42.911Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/07/8c/14c2ae915e5f9dca5a22edd68b35be94400719ccfa068a03e0fb63d0f6f6/mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0", size = 92796, upload-time = "2025-03-03T12:31:42.911Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/bd/d9/617e6af809bf3a1d468e0d58c3997b1dc219a9a9202e650d30c2fc85d481/mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f", size = 31617, upload_time = "2025-03-03T12:31:41.518Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/d9/617e6af809bf3a1d468e0d58c3997b1dc219a9a9202e650d30c2fc85d481/mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f", size = 31617, upload-time = "2025-03-03T12:31:41.518Z" },
]
[[package]]
name = "pycodestyle"
-version = "2.12.1"
+version = "2.14.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232, upload_time = "2024-08-04T20:26:54.576Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284, upload_time = "2024-08-04T20:26:53.173Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" },
]
[[package]]
name = "pyflakes"
-version = "3.2.0"
+version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788, upload_time = "2024-01-05T00:28:47.703Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload_time = "2024-01-05T00:28:45.903Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" },
]
[[package]]
name = "pygelf"
-version = "0.4.2"
+version = "0.4.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fe/d3/73d1fe74a156f9a0e519bedc87815ed309e64af19c73b94352e4c0959ddb/pygelf-0.4.2.tar.gz", hash = "sha256:d0bb8f45ff648a9a187713f4a05c09f685fcb8add7b04bb7471f20071bd11aad", size = 11991, upload_time = "2021-10-06T23:32:05.764Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/91/ac1605bb40092ae41fbb833ee55447f72e19ce5459efa6bd3beecc67e971/pygelf-0.4.3.tar.gz", hash = "sha256:8ed972563be3c8f168483f01dbf522b6bc697959c97a3f4881324b3f79638911", size = 11017, upload-time = "2025-06-14T19:21:19.832Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/03/cd/4afdddbc73f54ddf31d16137ef81c3d47192d75754b3115d925926081fd6/pygelf-0.4.2-py3-none-any.whl", hash = "sha256:ab57d1b26bffa014e29ae645ee51d2aa2f0c0cb419c522f2d24a237090b894a1", size = 8714, upload_time = "2021-10-06T23:32:03.156Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/ee/ebac3de919431912e0be380fafd01059a091a489f6b5d7896c2a04548895/pygelf-0.4.3-py3-none-any.whl", hash = "sha256:0876c99a77f9f021834982c9808205b3239fabf5886788d701f31b495b65c8ae", size = 8750, upload-time = "2025-06-14T19:21:16.953Z" },
]
[[package]]
@@ -135,89 +144,89 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload_time = "2024-03-01T18:36:20.211Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload_time = "2024-03-01T18:36:18.57Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "pytz"
-version = "2025.1"
+version = "2025.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617, upload_time = "2025-01-31T01:54:48.615Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930, upload_time = "2025-01-31T01:54:45.634Z" },
+ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload_time = "2024-12-04T17:35:28.174Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload_time = "2024-12-04T17:35:26.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "wrapt"
version = "1.17.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload_time = "2025-01-14T10:35:45.465Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307, upload_time = "2025-01-14T10:33:13.616Z" },
- { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486, upload_time = "2025-01-14T10:33:15.947Z" },
- { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777, upload_time = "2025-01-14T10:33:17.462Z" },
- { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314, upload_time = "2025-01-14T10:33:21.282Z" },
- { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947, upload_time = "2025-01-14T10:33:24.414Z" },
- { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778, upload_time = "2025-01-14T10:33:26.152Z" },
- { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716, upload_time = "2025-01-14T10:33:27.372Z" },
- { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548, upload_time = "2025-01-14T10:33:28.52Z" },
- { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334, upload_time = "2025-01-14T10:33:29.643Z" },
- { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427, upload_time = "2025-01-14T10:33:30.832Z" },
- { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774, upload_time = "2025-01-14T10:33:32.897Z" },
- { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload_time = "2025-01-14T10:33:33.992Z" },
- { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload_time = "2025-01-14T10:33:35.264Z" },
- { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload_time = "2025-01-14T10:33:38.28Z" },
- { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload_time = "2025-01-14T10:33:40.678Z" },
- { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload_time = "2025-01-14T10:33:41.868Z" },
- { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload_time = "2025-01-14T10:33:43.598Z" },
- { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload_time = "2025-01-14T10:33:48.499Z" },
- { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload_time = "2025-01-14T10:33:51.191Z" },
- { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload_time = "2025-01-14T10:33:52.328Z" },
- { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload_time = "2025-01-14T10:33:53.551Z" },
- { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload_time = "2025-01-14T10:33:56.323Z" },
- { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload_time = "2025-01-14T10:33:57.4Z" },
- { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload_time = "2025-01-14T10:33:59.334Z" },
- { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload_time = "2025-01-14T10:34:04.093Z" },
- { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload_time = "2025-01-14T10:34:07.163Z" },
- { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload_time = "2025-01-14T10:34:09.82Z" },
- { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload_time = "2025-01-14T10:34:11.258Z" },
- { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload_time = "2025-01-14T10:34:12.49Z" },
- { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload_time = "2025-01-14T10:34:15.043Z" },
- { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload_time = "2025-01-14T10:34:16.563Z" },
- { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload_time = "2025-01-14T10:34:17.727Z" },
- { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload_time = "2025-01-14T10:34:19.577Z" },
- { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload_time = "2025-01-14T10:34:21.571Z" },
- { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload_time = "2025-01-14T10:34:22.999Z" },
- { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload_time = "2025-01-14T10:34:25.386Z" },
- { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload_time = "2025-01-14T10:34:28.058Z" },
- { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload_time = "2025-01-14T10:34:29.167Z" },
- { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload_time = "2025-01-14T10:34:31.702Z" },
- { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload_time = "2025-01-14T10:34:32.91Z" },
- { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload_time = "2025-01-14T10:34:34.903Z" },
- { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload_time = "2025-01-14T10:34:36.13Z" },
- { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload_time = "2025-01-14T10:34:37.962Z" },
- { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload_time = "2025-01-14T10:34:39.13Z" },
- { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload_time = "2025-01-14T10:34:40.604Z" },
- { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload_time = "2025-01-14T10:34:45.011Z" },
- { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload_time = "2025-01-14T10:34:47.25Z" },
- { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload_time = "2025-01-14T10:34:50.934Z" },
- { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload_time = "2025-01-14T10:34:52.297Z" },
- { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload_time = "2025-01-14T10:34:53.489Z" },
- { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload_time = "2025-01-14T10:34:55.327Z" },
- { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload_time = "2025-01-14T10:34:58.055Z" },
- { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload_time = "2025-01-14T10:34:59.3Z" },
- { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload_time = "2025-01-14T10:35:00.498Z" },
- { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload_time = "2025-01-14T10:35:03.378Z" },
- { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload_time = "2025-01-14T10:35:44.018Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307, upload-time = "2025-01-14T10:33:13.616Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486, upload-time = "2025-01-14T10:33:15.947Z" },
+ { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777, upload-time = "2025-01-14T10:33:17.462Z" },
+ { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314, upload-time = "2025-01-14T10:33:21.282Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947, upload-time = "2025-01-14T10:33:24.414Z" },
+ { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778, upload-time = "2025-01-14T10:33:26.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716, upload-time = "2025-01-14T10:33:27.372Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548, upload-time = "2025-01-14T10:33:28.52Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334, upload-time = "2025-01-14T10:33:29.643Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427, upload-time = "2025-01-14T10:33:30.832Z" },
+ { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774, upload-time = "2025-01-14T10:33:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" },
+ { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" },
+ { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" },
+ { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" },
+ { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" },
+ { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" },
+ { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" },
+ { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" },
+ { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" },
+ { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" },
+ { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" },
+ { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" },
+ { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" },
+ { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" },
+ { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" },
+ { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" },
+ { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" },
+ { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" },
+ { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" },
+ { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" },
+ { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" },
]