mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-11 00:06:04 +00:00
chore: Add PyPI package support for uvx/pip installation (#1764)
* Initial plan * Add package structure and resource path utilities - Created langbot/ package with __init__.py and __main__.py entry point - Added paths utility to find frontend and resource files from package installation - Updated config loading to use resource paths - Updated frontend serving to use resource paths - Added MANIFEST.in for package data inclusion - Updated pyproject.toml with build system and entry points Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Add PyPI publishing workflow and update license - Created GitHub Actions workflow to build frontend and publish to PyPI - Added license field to pyproject.toml to fix deprecation warning - Updated .gitignore to exclude build artifacts - Tested package building successfully Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Add PyPI installation documentation - Created PYPI_INSTALLATION.md with detailed installation and usage instructions - Updated README.md to feature uvx/pip installation as recommended method - Updated README_EN.md with same changes for English documentation Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Address code review feedback - Made package-data configuration more specific to langbot package only - Improved path detection with caching to avoid repeated file I/O - Removed sys.path searching which was incorrect for package data - Removed interactive input() call for non-interactive environment compatibility - Simplified error messages for version check Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Fix code review issues - Use specific exception types instead of bare except - Fix misleading comments about directory levels - Remove redundant existence check before makedirs with exist_ok=True - Use context manager for file opening to ensure proper cleanup Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * Simplify package configuration and document behavioral differences - Removed redundant package-data configuration, relying on MANIFEST.in - Added documentation about behavioral differences between package and source installation - Clarified that include-package-data=true uses MANIFEST.in for data files Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> * chore: update pyproject.toml * chore: try pack templates in langbot/ * chore: update * chore: update * chore: update * chore: update * chore: update * chore: adjust dir structure * chore: fix imports * fix: read default-pipeline-config.json * fix: read default-pipeline-config.json * fix: tests * ci: publish pypi * chore: bump version 4.6.0-beta.1 for testing * chore: add templates/** * fix: send adapters and requesters icons * chore: bump version 4.6.0b2 for testing * chore: add platform field for docker-compose.yaml --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RockChinQ <45992437+RockChinQ@users.noreply.github.com> Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
0
src/langbot/pkg/config/__init__.py
Normal file
0
src/langbot/pkg/config/__init__.py
Normal file
0
src/langbot/pkg/config/impls/__init__.py
Normal file
0
src/langbot/pkg/config/impls/__init__.py
Normal file
71
src/langbot/pkg/config/impls/json.py
Normal file
71
src/langbot/pkg/config/impls/json.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import os
|
||||
import json
|
||||
import importlib.resources as resources
|
||||
|
||||
from langbot.pkg.config import model as file_model
|
||||
|
||||
|
||||
class JSONConfigFile(file_model.ConfigFile):
|
||||
"""JSON config file"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_file_name: str,
|
||||
template_resource_name: str = None,
|
||||
template_data: dict = None,
|
||||
) -> None:
|
||||
self.config_file_name = config_file_name
|
||||
self.template_resource_name = template_resource_name
|
||||
self.template_data = template_data
|
||||
|
||||
def exists(self) -> bool:
|
||||
return os.path.exists(self.config_file_name)
|
||||
|
||||
async def get_template_file_str(self) -> str:
|
||||
if self.template_resource_name is None:
|
||||
return None
|
||||
|
||||
with (
|
||||
resources.files('langbot.templates').joinpath(self.template_resource_name).open('r', encoding='utf-8') as f
|
||||
):
|
||||
return f.read()
|
||||
|
||||
async def create(self):
|
||||
if await self.get_template_file_str() is not None:
|
||||
with open(self.config_file_name, 'w', encoding='utf-8') as f:
|
||||
f.write(await self.get_template_file_str())
|
||||
elif self.template_data is not None:
|
||||
with open(self.config_file_name, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.template_data, f, indent=4, ensure_ascii=False)
|
||||
else:
|
||||
raise ValueError('template_file_name or template_data must be provided')
|
||||
|
||||
async def load(self, completion: bool = True) -> dict:
|
||||
if not self.exists():
|
||||
await self.create()
|
||||
|
||||
template_file_str = await self.get_template_file_str()
|
||||
|
||||
if template_file_str is not None:
|
||||
self.template_data = json.loads(template_file_str)
|
||||
|
||||
with open(self.config_file_name, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
cfg = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
raise Exception(f'Syntax error in config file {self.config_file_name}: {e}')
|
||||
|
||||
if completion:
|
||||
for key in self.template_data:
|
||||
if key not in cfg:
|
||||
cfg[key] = self.template_data[key]
|
||||
|
||||
return cfg
|
||||
|
||||
async def save(self, cfg: dict):
|
||||
with open(self.config_file_name, 'w', encoding='utf-8') as f:
|
||||
json.dump(cfg, f, indent=4, ensure_ascii=False)
|
||||
|
||||
def save_sync(self, cfg: dict):
|
||||
with open(self.config_file_name, 'w', encoding='utf-8') as f:
|
||||
json.dump(cfg, f, indent=4, ensure_ascii=False)
|
||||
66
src/langbot/pkg/config/impls/pymodule.py
Normal file
66
src/langbot/pkg/config/impls/pymodule.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import os
|
||||
import shutil
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
from .. import model as file_model
|
||||
|
||||
|
||||
class PythonModuleConfigFile(file_model.ConfigFile):
|
||||
"""Python module config file"""
|
||||
|
||||
config_file_name: str = None
|
||||
"""Config file name"""
|
||||
|
||||
template_file_name: str = None
|
||||
"""Template file name"""
|
||||
|
||||
def __init__(self, config_file_name: str, template_file_name: str) -> None:
|
||||
self.config_file_name = config_file_name
|
||||
self.template_file_name = template_file_name
|
||||
|
||||
def exists(self) -> bool:
|
||||
return os.path.exists(self.config_file_name)
|
||||
|
||||
async def create(self):
|
||||
shutil.copyfile(self.template_file_name, self.config_file_name)
|
||||
|
||||
async def load(self, completion: bool = True) -> dict:
|
||||
module_name = os.path.splitext(os.path.basename(self.config_file_name))[0]
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
cfg = {}
|
||||
|
||||
allowed_types = (int, float, str, bool, list, dict)
|
||||
|
||||
for key in dir(module):
|
||||
if key.startswith('__'):
|
||||
continue
|
||||
|
||||
if not isinstance(getattr(module, key), allowed_types):
|
||||
continue
|
||||
|
||||
cfg[key] = getattr(module, key)
|
||||
|
||||
# complete from template module file
|
||||
if completion:
|
||||
module_name = os.path.splitext(os.path.basename(self.template_file_name))[0]
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
for key in dir(module):
|
||||
if key.startswith('__'):
|
||||
continue
|
||||
|
||||
if not isinstance(getattr(module, key), allowed_types):
|
||||
continue
|
||||
|
||||
if key not in cfg:
|
||||
cfg[key] = getattr(module, key)
|
||||
|
||||
return cfg
|
||||
|
||||
async def save(self, data: dict):
|
||||
logging.warning('Python module config file does not support saving')
|
||||
|
||||
def save_sync(self, data: dict):
|
||||
logging.warning('Python module config file does not support saving')
|
||||
71
src/langbot/pkg/config/impls/yaml.py
Normal file
71
src/langbot/pkg/config/impls/yaml.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import os
|
||||
import yaml
|
||||
import importlib.resources as resources
|
||||
|
||||
from langbot.pkg.config import model as file_model
|
||||
|
||||
|
||||
class YAMLConfigFile(file_model.ConfigFile):
|
||||
"""YAML config file"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_file_name: str,
|
||||
template_resource_name: str = None,
|
||||
template_data: dict = None,
|
||||
) -> None:
|
||||
self.config_file_name = config_file_name
|
||||
self.template_resource_name = template_resource_name
|
||||
self.template_data = template_data
|
||||
|
||||
def exists(self) -> bool:
|
||||
return os.path.exists(self.config_file_name)
|
||||
|
||||
async def get_template_file_str(self) -> str:
|
||||
if self.template_resource_name is None:
|
||||
return None
|
||||
|
||||
with (
|
||||
resources.files('langbot.templates').joinpath(self.template_resource_name).open('r', encoding='utf-8') as f
|
||||
):
|
||||
return f.read()
|
||||
|
||||
async def create(self):
|
||||
if await self.get_template_file_str() is not None:
|
||||
with open(self.config_file_name, 'w', encoding='utf-8') as f:
|
||||
f.write(await self.get_template_file_str())
|
||||
elif self.template_data is not None:
|
||||
with open(self.config_file_name, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(self.template_data, f, indent=4, allow_unicode=True)
|
||||
else:
|
||||
raise ValueError('template_file_name or template_data must be provided')
|
||||
|
||||
async def load(self, completion: bool = True) -> dict:
|
||||
if not self.exists():
|
||||
await self.create()
|
||||
|
||||
template_file_str = await self.get_template_file_str()
|
||||
|
||||
if template_file_str is not None:
|
||||
self.template_data = yaml.load(template_file_str, Loader=yaml.FullLoader)
|
||||
|
||||
with open(self.config_file_name, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
cfg = yaml.load(f, Loader=yaml.FullLoader)
|
||||
except yaml.YAMLError as e:
|
||||
raise Exception(f'Syntax error in config file {self.config_file_name}: {e}')
|
||||
|
||||
if completion:
|
||||
for key in self.template_data:
|
||||
if key not in cfg:
|
||||
cfg[key] = self.template_data[key]
|
||||
|
||||
return cfg
|
||||
|
||||
async def save(self, cfg: dict):
|
||||
with open(self.config_file_name, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(cfg, f, indent=4, allow_unicode=True)
|
||||
|
||||
def save_sync(self, cfg: dict):
|
||||
with open(self.config_file_name, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(cfg, f, indent=4, allow_unicode=True)
|
||||
107
src/langbot/pkg/config/manager.py
Normal file
107
src/langbot/pkg/config/manager.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from . import model as file_model
|
||||
from .impls import pymodule, json as json_file, yaml as yaml_file
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""Config file manager"""
|
||||
|
||||
name: str = None
|
||||
"""Config manager name"""
|
||||
|
||||
description: str = None
|
||||
"""Config manager description"""
|
||||
|
||||
schema: dict = None
|
||||
"""Config file schema
|
||||
Must conform to JSON Schema Draft 7 specification
|
||||
"""
|
||||
|
||||
file: file_model.ConfigFile = None
|
||||
"""Config file instance"""
|
||||
|
||||
data: dict = None
|
||||
"""Config data"""
|
||||
|
||||
doc_link: str = None
|
||||
"""Config file documentation link"""
|
||||
|
||||
def __init__(self, cfg_file: file_model.ConfigFile) -> None:
|
||||
self.file = cfg_file
|
||||
self.data = {}
|
||||
|
||||
async def load_config(self, completion: bool = True):
|
||||
self.data = await self.file.load(completion=completion)
|
||||
|
||||
async def dump_config(self):
|
||||
await self.file.save(self.data)
|
||||
|
||||
def dump_config_sync(self):
|
||||
self.file.save_sync(self.data)
|
||||
|
||||
|
||||
async def load_python_module_config(config_name: str, template_name: str, completion: bool = True) -> ConfigManager:
|
||||
"""Load Python module config file
|
||||
|
||||
Args:
|
||||
config_name (str): Config file name
|
||||
template_name (str): Template file name
|
||||
completion (bool): Whether to automatically complete the config file in memory
|
||||
|
||||
Returns:
|
||||
ConfigManager: Config file manager
|
||||
"""
|
||||
cfg_inst = pymodule.PythonModuleConfigFile(config_name, template_name)
|
||||
|
||||
cfg_mgr = ConfigManager(cfg_inst)
|
||||
await cfg_mgr.load_config(completion=completion)
|
||||
|
||||
return cfg_mgr
|
||||
|
||||
|
||||
async def load_json_config(
|
||||
config_name: str,
|
||||
template_resource_name: str = None,
|
||||
template_data: dict = None,
|
||||
completion: bool = True,
|
||||
) -> ConfigManager:
|
||||
"""Load JSON config file
|
||||
|
||||
Args:
|
||||
config_name (str): Config file name
|
||||
template_resource_name (str): Template resource name
|
||||
template_data (dict): Template data
|
||||
completion (bool): Whether to automatically complete the config file in memory
|
||||
"""
|
||||
cfg_inst = json_file.JSONConfigFile(config_name, template_resource_name, template_data)
|
||||
|
||||
cfg_mgr = ConfigManager(cfg_inst)
|
||||
await cfg_mgr.load_config(completion=completion)
|
||||
|
||||
return cfg_mgr
|
||||
|
||||
|
||||
async def load_yaml_config(
|
||||
config_name: str,
|
||||
template_resource_name: str = None,
|
||||
template_data: dict = None,
|
||||
completion: bool = True,
|
||||
) -> ConfigManager:
|
||||
"""Load YAML config file
|
||||
|
||||
Args:
|
||||
config_name (str): Config file name
|
||||
template_resource_name (str): Template resource name
|
||||
template_data (dict): Template data
|
||||
completion (bool): Whether to automatically complete the config file in memory
|
||||
|
||||
Returns:
|
||||
ConfigManager: Config file manager
|
||||
"""
|
||||
cfg_inst = yaml_file.YAMLConfigFile(config_name, template_resource_name, template_data)
|
||||
|
||||
cfg_mgr = ConfigManager(cfg_inst)
|
||||
await cfg_mgr.load_config(completion=completion)
|
||||
|
||||
return cfg_mgr
|
||||
34
src/langbot/pkg/config/model.py
Normal file
34
src/langbot/pkg/config/model.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import abc
|
||||
|
||||
|
||||
class ConfigFile(metaclass=abc.ABCMeta):
|
||||
"""Config file abstract class"""
|
||||
|
||||
config_file_name: str = None
|
||||
"""Config file name"""
|
||||
|
||||
template_file_name: str = None
|
||||
"""Template file name"""
|
||||
|
||||
template_data: dict = None
|
||||
"""Template data"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def exists(self) -> bool:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def create(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def load(self, completion: bool = True) -> dict:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def save(self, data: dict):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def save_sync(self, data: dict):
|
||||
pass
|
||||
Reference in New Issue
Block a user