Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
TZ: Asia/Shanghai

Expand Down
9 changes: 0 additions & 9 deletions doc/en/api/time.md

This file was deleted.

35 changes: 35 additions & 0 deletions doc/en/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,38 @@
::: aloha.util.sys_gpu

::: aloha.util.sys_info

## Time Utilities (`aloha.util.time`)

This module provides tools for wrapping function calls (such as HTTP requests via `requests` or `httpx`) with time constraints (timeouts), allowing execution of optional callbacks upon completion or failure.

### Key Functions
- `run_with_timeout`: Wrap a synchronous function call with a timeout.
- `run_async_with_timeout`: Wrap an asynchronous function call with a timeout.

### Usage Example
```python
from aloha.util.time import run_with_timeout
import requests

def success_callback(response):
print("Request succeeded:", response.status_code)

def fail_callback(exception):
print("Request failed or timed out:", exception)

# Synchronous call with timeout
try:
run_with_timeout(
requests.get,
2.5, # 2.5 seconds timeout
"https://httpbin.org/delay/1",
fn_callback_success=success_callback,
fn_callback_fail=fail_callback
)
except TimeoutError:
print("Caught TimeoutError")
```

::: aloha.util.time

1 change: 0 additions & 1 deletion doc/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,5 @@ nav:
- Service: "api/service.md"
- Encryption: "api/encrypt.md"
- Database: "api/db.md"
- Time: "api/time.md"
- Utilities: "api/util.md"
- Testing: "api/testing.md"
1 change: 0 additions & 1 deletion doc/mkdocs.zh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,5 @@ nav:
- 服务层: "api/service.md"
- 加密: "api/encrypt.md"
- 数据库: "api/db.md"
- 时间工具: "api/time.md"
- 工具函数: "api/util.md"
- 测试工具: "api/testing.md"
17 changes: 16 additions & 1 deletion doc/skills/project_scaffolding.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The `aloha-python` repository is organized into several top-level directories, e

- **`pkg/`**: This directory stores the source code for the `aloha` Python package that is intended for publication to PyPI. It is the correct place to modify when the task is to work on this library itself. When using this repository as a boilerplate for a new application project, developers or agents should not include this directory unless they explicitly intend to create and publish a new package to PyPI.

- **`src/`**: This directory is designed for the application-specific code that consumes the `aloha` package. It serves as a boilerplate example (`app_common`) for how to structure a project using `aloha`. New projects based on this boilerplate should place their primary application logic and modules here.
- **`src/`**: This directory is designed for application-specific code and tests that consume the `aloha` package. It serves as a boilerplate example (`app_common`) for how to structure a project using `aloha`. New projects based on this boilerplate should place their primary application logic, modules, and tests here.

- **`notebook/`**: This directory is for Jupyter notebooks, which can be used for experimentation, data analysis, or interactive development related to the project.

Expand All @@ -30,5 +30,20 @@ To initiate a new project using `aloha-python` as a boilerplate, follow these st
- **`src/` for Application Logic**: All primary application code, including API handlers, business logic, and utility modules, should reside within `src/`. The `src/main.py` script acts as a generic entry point for running Python modules within the `src/` directory. Your application's main function should be callable via `python3 src/main.py your_module.main`.
- **`pkg/` is not part of a new boilerplate app**: If the goal is to build a new application project from this repository, do not carry over `pkg/` unless the user specifically wants to create and publish a separate package. Application code should live in `src/` instead.
- **`resource/config/` for Configuration**: Application configuration files (e.g., `main.conf`, `deploy-DEV.conf`) should be placed under `src/resource/config/`. The `aloha` package's `aloha.config.paths` module handles the discovery and loading of these configuration files. For detailed information on HOCON configuration, refer to the "Configuration with HOCON" section in the `aloha_package_usage.md` skill.
- **Tests Placement**: All test-related code (including unit tests, integration tests, and test resources) must be placed inside the `src/` directory, typically organized under a `src/tests/` subdirectory. Test files should follow standard naming conventions such as `test_*.py`.
- **Executing Tests**: Tests should be run using `pytest` inside the containerized development environment:
1. Launch and enter the development container:
```bash
./tool/cicd/run-dev.sh up
./tool/cicd/run-dev.sh enter
```
2. Run tests under the `src/` directory:
```bash
pytest src/
```
3. To run tests with code coverage analysis:
```bash
pytest --cov=src src/
```

By adhering to these conventions, AI agents can effectively understand, navigate, and contribute to projects built upon the `aloha-python` framework.
9 changes: 0 additions & 9 deletions doc/zh/api/time.md

This file was deleted.

35 changes: 35 additions & 0 deletions doc/zh/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,38 @@
::: aloha.util.sys_gpu

::: aloha.util.sys_info

## 时间工具 (`aloha.util.time`)

该模块提供用于包装函数调用(如通过 `requests` 或 `httpx` 发起外部 HTTP 请求)的超时控制工具,并在操作成功或失败(超时/异常)时触发可选的回调函数。

### 核心函数
- `run_with_timeout`: 以同步方式运行函数,并应用超时限制。
- `run_async_with_timeout`: 以异步方式(协程或在执行器中运行同步函数)运行函数,并应用超时限制。

### 使用示例
```python
from aloha.util.time import run_with_timeout
import requests

def success_callback(response):
print("请求成功:", response.status_code)

def fail_callback(exception):
print("请求失败或超时:", exception)

# 同步超时包装调用
try:
run_with_timeout(
requests.get,
2.5, # 2.5 秒超时限制
"https://httpbin.org/delay/1",
fn_callback_success=success_callback,
fn_callback_fail=fail_callback
)
except TimeoutError:
print("捕获到超时异常 (TimeoutError)")
```

::: aloha.util.time

20 changes: 16 additions & 4 deletions pkg/aloha/db/elasticsearch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Elasticsearch connection helpers."""

import json
import re

from elasticsearch import Elasticsearch

Expand All @@ -10,6 +9,16 @@
__all__ = ("ElasticSearchOperator",)


def _mask_hosts(hosts):
if isinstance(hosts, list):
return [_mask_hosts(h) for h in hosts]
if isinstance(hosts, dict):
return {k: ("***" if k in ("password", "http_auth") else _mask_hosts(v)) for k, v in hosts.items()}
if isinstance(hosts, str):
return re.sub(r"([^:/]+://)?([^:/]+):([^@]+)@", r"\1\2:***@", hosts)
return hosts


class ElasticSearchOperator:
"""Create and use an Elasticsearch client with optional index helpers."""

Expand All @@ -21,14 +30,17 @@ def __init__(self, config, index_config=None):
username = config.get("username")
password = password_vault.get_password(config.get("password"))

hosts = config.get("host", "localhost")
masked_hosts = _mask_hosts(hosts)
LOG.debug("ElasticSearch connection info: " + str(masked_hosts))

self._config = {
"http_auth": (username, password) if username is not None and password is not None else None,
"hosts": config.get("host", "localhost"),
"hosts": hosts,
"timeout": config.get("timeout", 0.1),
"max_retries": config.get("max_retries", 3),
"retry_on_timeout": config.get("retry_on_timeout", True),
}
LOG.debug("ElasticSearch connection info: " + str(self._config["hosts"]))

self.index_config = index_config
self.index_name = self.es_config.get("index_name")
Expand Down
3 changes: 2 additions & 1 deletion pkg/aloha/db/mongo.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
"maxPoolSize": config.get("maxPoolSize"),
"authSource": config.get("authSource", db_name),
}
LOG.debug(_config)
msg = {k: ("***" if k == "password" else v) for k, v in _config.items()}
LOG.debug(msg)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (password)
as clear text.
This expression logs
sensitive data (password)
as clear text.

try:
self.conn = pymongo.MongoClient(**_config)
Expand Down
5 changes: 3 additions & 2 deletions pkg/aloha/service/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .api import v0, v1, v2
from .http import DefaultHandler404
from .handlers import DefaultHandler404
from .http import CORSMiddleware

__all__ = ("DefaultHandler404", "v0", "v1", "v2")
__all__ = ("CORSMiddleware", "DefaultHandler404", "v0", "v1", "v2")
85 changes: 76 additions & 9 deletions pkg/aloha/service/api/v0.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
"""Version 0 JSON API helpers.
"""Version 0 JSON API helpers for FastAPI.

This module defines the simplest request/response protocol used by aloha:
request bodies are passed directly to the handler method and the response is
serialized as a JSON object with a `code` and `message` field.
"""

import json
import logging
from abc import ABC

from ..http import AbstractApiClient, AbstractApiHandler
from fastapi import Request
from fastapi.responses import JSONResponse

__all__ = ("APIHandler", "APICaller")
from ..http import AbstractApiClient
from ..http.base_api_handler import AbstractApiHandler as BaseHandler

__all__ = ("APIHandler", "APICaller", "create_v0_router")

class APIHandler(AbstractApiHandler, ABC):
"""Base Tornado handler for v0 JSON endpoints.

class APIHandler(BaseHandler, ABC):
"""Base handler for v0 JSON endpoints using FastAPI.

Subclasses implement :meth:`response`, which receives parsed request data
and returns a Python object that can be JSON-serialized.
Expand All @@ -27,21 +30,85 @@ async def post(self, *args, **kwargs):
"""Parse the request body, call :meth:`response`, and return JSON."""
req_body = self.request_body

if req_body is not None: # body_arguments
if req_body is not None:
kwargs.update(req_body)

resp = dict(code=5200, message=["success"])
try:
result = self.response(*args, **kwargs) # this call may throw TypeError when argument missing
result = self.response(*args, **kwargs)
resp["data"] = result
except Exception as e:
if self.LOG.level == logging.DEBUG:
self.LOG.error(e, exc_info=True)
return self.finish({"code": 5201, "message": [repr(e)]})

resp = json.dumps(resp, ensure_ascii=False, default=str, separators=(",", ":"))
return self.finish(resp)

async def get(self, *args, **kwargs):
"""Handle GET request (useful for some v0 endpoints)."""
kwargs.update(self.request_param)
resp = dict(code=5200, message=["success"])
try:
result = self.response(*args, **kwargs)
resp["data"] = result
except Exception as e:
if self.LOG.level == logging.DEBUG:
self.LOG.error(e, exc_info=True)
return self.finish({"code": 5201, "message": [repr(e)]})
return self.finish(resp)


def create_v0_router(handler_class):
"""Create FastAPI routes for a v0 API handler class.

Args:
handler_class: A class inheriting from APIHandler

Returns:
A function that registers routes on a FastAPI app
"""

async def handle_post(request: Request, **kwargs):
handler = handler_class()
handler._request = request

# Get body for POST
try:
body = await request.json()
except Exception:
body = {}

kwargs.update(body)
resp = dict(code=5200, message=["success"])
try:
result = handler.response(**kwargs)
resp["data"] = result
except Exception as e:
if handler.LOG.level == logging.DEBUG:
handler.LOG.error(e, exc_info=True)
return JSONResponse({"code": 5201, "message": [repr(e)]}, status_code=500)

return JSONResponse(resp)

async def handle_get(request: Request, **kwargs):
handler = handler_class()
handler._request = request

# Get query params for GET
kwargs.update(dict(request.query_params))
resp = dict(code=5200, message=["success"])
try:
result = handler.response(**kwargs)
resp["data"] = result
except Exception as e:
if handler.LOG.level == logging.DEBUG:
handler.LOG.error(e, exc_info=True)
return JSONResponse({"code": 5201, "message": [repr(e)]}, status_code=500)

return JSONResponse(resp)

return handle_post, handle_get


class APICaller(AbstractApiClient):
"""Client helper for v0 endpoints.
Expand Down
Loading