Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

uv 💜 #4

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ jobs:
run: |
docker load --input /tmp/${{ env.KBC_DEVELOPERPORTAL_APP }}.tar
docker image ls -a
docker run ${{ env.KBC_DEVELOPERPORTAL_APP }}:latest flake8 . --config=flake8.cfg
docker run ${{ env.KBC_DEVELOPERPORTAL_APP }}:latest uv run --active flake8 . --config=flake8.cfg
echo "Running unit-tests..."
docker run ${{ env.KBC_DEVELOPERPORTAL_APP }}:latest python -m unittest discover
docker run ${{ env.KBC_DEVELOPERPORTAL_APP }}:latest uv run --active python -m unittest discover

tests-kbc:
name: Run KBC Tests
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ __pycache__/

# kbc datafolder
/data
/venv
/venv
test.py
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.10
39 changes: 13 additions & 26 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,34 +1,21 @@
FROM quay.io/keboola/docker-custom-python:latest
ENV PYTHONIOENCODING utf-8



# Create directory for user packages
# This directory is usually created automatically by pip
# ... but if it doesn't exist when you run the script
# ... then the modules installed during the transformation are not loaded automatically!
# ... because the loader thinks that this directory does not exist (it did not exist at the start of the script)
# Eg. mkdir -p /var/www/.local/lib/python3.8/site-packages
RUN mkdir -p $(su www-data -s /bin/bash -c "python -c 'import site; print(site.USER_SITE)'")

# Make home directory writable
RUN chown -R www-data:www-data /var/www

COPY /src /code/src/
COPY /tests /code/tests/
COPY /scripts /code/scripts/
COPY requirements.txt /code/requirements.txt
COPY flake8.cfg /code/flake8.cfg
COPY deploy.sh /code/deploy.sh

# install gcc to be able to build packages - e.g. required by regex, dateparser, also required for pandas
RUN apt-get update && apt-get install -y build-essential

RUN pip install flake8
# RUN apt-get update && apt-get install -y build-essential

RUN pip install -r /code/requirements.txt
RUN pip install uv

WORKDIR /code/

COPY pyproject.toml .
COPY uv.lock .

RUN uv pip sync --system pyproject.toml

COPY src/ src/
COPY tests/ tests/
COPY scripts/ scripts/
COPY flake8.cfg .
COPY deploy.sh .

CMD ["python", "-u", "/code/src/component.py"]
CMD uv run --active /code/src/component.py
3 changes: 1 addition & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: "2"
services:
# for development purposes
dev:
Expand All @@ -7,7 +6,7 @@ services:
- ./:/code
- ./data:/data
environment:
- KBC_DATADIR=./data
- KBC_DATADIR=/data
test:
# Use to run flake8 and unittests checks
build: .
Expand Down
1 change: 1 addition & 0 deletions flake8.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[flake8]
exclude =
.git,
.venv,
__pycache__,
tests,
example
Expand Down
18 changes: 18 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[project]
name = "custom-python"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"dlt>=1.6.1",
"duckdb>=1.2.0",
"keboola-component>=1.6.10",
]

[dependency-groups]
dev = [
"flake8>=7.1.2",
"freezegun>=1.5.1",
"mock>=5.2.0",
]
7 changes: 0 additions & 7 deletions requirements.txt

This file was deleted.

4 changes: 2 additions & 2 deletions scripts/build_n_test.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/sh
set -e

flake8 --config=flake8.cfg
python -m unittest discover
uv run --active flake8 --config=flake8.cfg
uv run --active python -m unittest discover
72 changes: 33 additions & 39 deletions src/component.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
"""
Template Component main class.

"""

import json
import logging
import os
import runpy
import subprocess
import sys
import traceback
from traceback import TracebackException

from keboola.component.base import ComponentBase
from keboola.component.exceptions import UserException

# configuration variables
KEY_API_TOKEN = '#api_token'
KEY_PRINT_HELLO = 'print_hello'
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.
Expand All @@ -24,13 +26,13 @@

class Component(ComponentBase):
"""
Extends base class for general Python components. Initializes the CommonInterface
and performs configuration validation.
Extends base class for general Python components. Initializes the CommonInterface
and performs configuration validation.

For easier debugging the data folder is picked up by default from `../data` path,
relative to working directory.
For easier debugging the data folder is picked up by default from `../data` path,
relative to working directory.

If `debug` parameter is present in the `config.json`, the default logger is set to verbose DEBUG mode.
If `debug` parameter is present in the `config.json`, the default logger is set to verbose DEBUG mode.
"""

def __init__(self):
Expand All @@ -40,41 +42,40 @@ def run(self):
parameters = self.configuration.parameters

self._set_init_logging_handler()
script_path = os.path.join(self.data_folder_path, 'script.py')
logging.info(sys.executable)
Copy link
Preview

Copilot AI Mar 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logging of sys.executable appears to be a leftover debug statement; consider removing it or wrapping it in a conditional to avoid unintended output in production.

Suggested change
logging.info(sys.executable)
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
logging.debug(sys.executable)

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
script_path = os.path.join(self.data_folder_path, "script.py")
self.prepare_script_file(script_path)

self._merge_user_parameters()

# install packages
self.install_packages(parameters.get('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:
script = self.configuration.parameters["code"]
with open(destination_path, "w+") as file:
file.write(script)

def execute_script_file(self, file_path):
import traceback

# Change current working directory so that relative paths work
os.chdir(self.data_folder_path)
sys.path.append(self.data_folder_path)

try:
with open(file_path, 'r') as file:
with open(file_path, "r") as file:
script = file.read()
logging.info('Execute script "%s"' % (self.script_excerpt(script)))
logging.info('Executing script "%s"', self.script_excerpt(script))
runpy.run_path(file_path)
logging.info('Script finished')
logging.info("Script finished")
except Exception as err:
_, _, tb = sys.exc_info()
stack_len = len(traceback.extract_tb(tb)[4:])
stack_trace_records = self._get_stack_trace_records(*sys.exc_info(), -stack_len, chain=True)
stack_cropped = "\n".join(stack_trace_records)

raise UserException(f'Script failed. {err}. Detail: {stack_cropped}') from err
raise UserException(f"Script failed. {err}. Detail: {stack_cropped}") from err

@staticmethod
def _get_stack_trace_records(etype, value, tb, limit=None, chain=True):
Expand All @@ -86,64 +87,57 @@ 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]
return script[0:500] + "\n...\n" + script[-500]
else:
return script

@staticmethod
def install_packages(packages):
import subprocess
import sys
for package in packages:
args = [
sys.executable,
'-m', 'pip', 'install',
'--disable-pip-version-check',
'--no-cache-dir',
'--no-warn-script-location', # ignore error: installed in '/var/www/.local/bin' which is not on PATH.
'--force-reinstall',
package
"uv",
"pip",
"install",
package,
]
process = subprocess.Popen(args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
logging.info(f'Installing package: {package}. Full log in detail.', extra={'full_message': stdout})
logging.info(f"Installing package: {package}. Full log in detail.", extra={"full_message": stdout})
process.poll()
if process.poll() != 0:
raise UserException('Failed to install package: {package}. Log in event detail.', stderr)
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("[Non-script message]: %(message)s"))

def _merge_user_parameters(self):
"""
INPLACE Merges user paramters into config.json->parameters property. Rebuilds the physical config.json file
INPLACE Merges user parameters into config.json->parameters property. Rebuilds the physical config.json file
Returns:

"""
# remove code
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.configuration.parameters.get("user_properties", {})
with open(os.path.join(self.data_folder_path, "config.json"), "w+") as inp:
json.dump(config_data, inp)


"""
Main entrypoint
Main entrypoint
"""
if __name__ == "__main__":
try:
comp = Component()
# this triggers the run method by default and is controlled by the configuration.action parameter
comp.execute_action()
except UserException as exc:
detail = ''
detail = ""
if len(exc.args) > 1:
detail = exc.args[1]
logging.exception(exc, extra={"full_message": detail})
Expand Down
Loading