mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
7 Commits
add-github
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85b959d883 | ||
|
|
1ee548b909 | ||
|
|
21aa52ce3b | ||
|
|
1e023ce56b | ||
|
|
5b500d640a | ||
|
|
c824b2dda5 | ||
|
|
c9b6f54e76 |
@@ -27,7 +27,7 @@ Before pushing any changes, you MUST ensure that any lint errors or simple test
|
||||
|
||||
* If you've made changes to the backend, you should run `pre-commit run --config ./dev_config/python/.pre-commit-config.yaml` (this will run on staged files).
|
||||
* If you've made changes to the frontend, you should run `cd frontend && npm run lint:fix && npm run build ; cd ..`
|
||||
* If you've made changes to the VSCode extension, you should run `cd openhands/integrations/vscode && npm run lint:fix && npm run compile ; cd ../../..`
|
||||
* If you've made changes to the VSCode extension, you should run `cd openhands/app_server/integrations/vscode && npm run lint:fix && npm run compile ; cd ../../..`
|
||||
|
||||
The pre-commit hooks MUST pass successfully before pushing any changes to the repository. This is a mandatory requirement to maintain code quality and consistency.
|
||||
|
||||
@@ -150,7 +150,7 @@ Frontend:
|
||||
|
||||
|
||||
VSCode Extension:
|
||||
- Located in the `openhands/integrations/vscode` directory
|
||||
- Located in the `openhands/app_server/integrations/vscode` directory
|
||||
- Setup: Run `npm install` in the extension directory
|
||||
- Linting:
|
||||
- Run linting with fixes: `npm run lint:fix`
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from pydantic import SecretStr
|
||||
from server.auth.token_manager import TokenManager
|
||||
|
||||
from openhands.app_server.integrations.bitbucket.bitbucket_service import (
|
||||
BitBucketService,
|
||||
)
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.bitbucket.bitbucket_service import BitBucketService
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
|
||||
|
||||
class SaaSBitBucketService(BitBucketService):
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from pydantic import SecretStr
|
||||
from server.auth.token_manager import TokenManager
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.bitbucket_data_center.bitbucket_dc_service import (
|
||||
from openhands.app_server.integrations.bitbucket_data_center.bitbucket_dc_service import (
|
||||
BitbucketDCService,
|
||||
)
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
class SaaSBitbucketDCService(BitbucketDCService):
|
||||
|
||||
@@ -19,12 +19,12 @@ from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
|
||||
from storage.openhands_pr import OpenhandsPR
|
||||
from storage.openhands_pr_store import OpenhandsPRStore
|
||||
|
||||
from openhands.app_server.conversation_paths import get_conversation_dir
|
||||
from openhands.app_server.file_store import get_file_store
|
||||
from openhands.app_server.integrations.github.github_service import GithubServiceImpl
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.core.config import load_openhands_config
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.github.github_service import GithubServiceImpl
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.storage import get_file_store
|
||||
from openhands.storage.locations import get_conversation_dir
|
||||
|
||||
config = load_openhands_config()
|
||||
file_store = get_file_store(config.file_store, config.file_store_path)
|
||||
@@ -112,7 +112,7 @@ class GitHubDataCollector:
|
||||
suffix = path.format(repo_id, number)
|
||||
|
||||
if conversation_id:
|
||||
return f'{get_conversation_dir(conversation_id)}{suffix}'
|
||||
return f'{get_conversation_dir(conversation_id)}/{suffix}'
|
||||
|
||||
return suffix
|
||||
|
||||
|
||||
@@ -31,10 +31,10 @@ from server.auth.auth_error import ExpiredError
|
||||
from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
|
||||
from server.auth.token_manager import TokenManager
|
||||
|
||||
from openhands.app_server.integrations.provider import ProviderToken, ProviderType
|
||||
from openhands.app_server.integrations.service_types import AuthenticationError
|
||||
from openhands.app_server.secrets.secrets_models import Secrets
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.provider import ProviderToken, ProviderType
|
||||
from openhands.integrations.service_types import AuthenticationError
|
||||
from openhands.server.types import (
|
||||
LLMAuthenticationError,
|
||||
MissingSettingsError,
|
||||
|
||||
@@ -4,9 +4,9 @@ from integrations.store_repo_utils import store_repositories_in_db
|
||||
from pydantic import SecretStr
|
||||
from server.auth.token_manager import TokenManager
|
||||
|
||||
from openhands.app_server.integrations.github.github_service import GitHubService
|
||||
from openhands.app_server.integrations.service_types import ProviderType, Repository
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.github.github_service import GitHubService
|
||||
from openhands.integrations.service_types import ProviderType, Repository
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
|
||||
|
||||
@@ -34,14 +34,14 @@ from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
ConversationTrigger,
|
||||
)
|
||||
from openhands.app_server.config import get_app_conversation_service
|
||||
from openhands.app_server.integrations.github.github_service import GithubServiceImpl
|
||||
from openhands.app_server.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
|
||||
from openhands.app_server.integrations.service_types import Comment
|
||||
from openhands.app_server.services.injector import InjectorState
|
||||
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.github.github_service import GithubServiceImpl
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
|
||||
from openhands.integrations.service_types import Comment
|
||||
from openhands.sdk import TextContent
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
OH_LABEL, INLINE_OH_LABEL = get_oh_labels(HOST)
|
||||
|
||||
@@ -25,10 +25,10 @@ from jinja2 import Environment, FileSystemLoader
|
||||
from pydantic import SecretStr
|
||||
from server.auth.token_manager import TokenManager
|
||||
|
||||
from openhands.app_server.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
from openhands.app_server.integrations.provider import ProviderToken, ProviderType
|
||||
from openhands.app_server.secrets.secrets_models import Secrets
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
from openhands.integrations.provider import ProviderToken, ProviderType
|
||||
from openhands.server.types import (
|
||||
LLMAuthenticationError,
|
||||
MissingSettingsError,
|
||||
|
||||
@@ -7,14 +7,14 @@ from server.auth.token_manager import TokenManager
|
||||
from storage.gitlab_webhook import GitlabWebhook, WebhookStatus
|
||||
from storage.gitlab_webhook_store import GitlabWebhookStore
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.gitlab.gitlab_service import GitLabService
|
||||
from openhands.integrations.service_types import (
|
||||
from openhands.app_server.integrations.gitlab.gitlab_service import GitLabService
|
||||
from openhands.app_server.integrations.service_types import (
|
||||
ProviderType,
|
||||
RateLimitError,
|
||||
Repository,
|
||||
RequestMethod,
|
||||
)
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
|
||||
|
||||
@@ -22,14 +22,14 @@ from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
ConversationTrigger,
|
||||
)
|
||||
from openhands.app_server.config import get_app_conversation_service
|
||||
from openhands.app_server.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
from openhands.app_server.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
|
||||
from openhands.app_server.integrations.service_types import Comment
|
||||
from openhands.app_server.services.injector import InjectorState
|
||||
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
|
||||
from openhands.integrations.service_types import Comment
|
||||
from openhands.sdk import TextContent
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
|
||||
OH_LABEL, INLINE_OH_LABEL = get_oh_labels(HOST)
|
||||
CONFIDENTIAL_NOTE = 'confidential_note'
|
||||
|
||||
@@ -42,13 +42,13 @@ from storage.jira_integration_store import JiraIntegrationStore
|
||||
from storage.jira_user import JiraUser
|
||||
from storage.jira_workspace import JiraWorkspace
|
||||
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.types import (
|
||||
LLMAuthenticationError,
|
||||
MissingSettingsError,
|
||||
SessionExpiredError,
|
||||
)
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.utils.http_session import httpx_verify_option
|
||||
|
||||
JIRA_CLOUD_API_URL = 'https://api.atlassian.com/ex/jira'
|
||||
|
||||
@@ -7,7 +7,7 @@ from jinja2 import Environment
|
||||
from storage.jira_user import JiraUser
|
||||
from storage.jira_workspace import JiraWorkspace
|
||||
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from integrations.jira.jira_payload import JiraWebhookPayload
|
||||
|
||||
@@ -38,12 +38,12 @@ from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
ConversationTrigger,
|
||||
)
|
||||
from openhands.app_server.config import get_app_conversation_service
|
||||
from openhands.app_server.integrations.provider import ProviderHandler, ProviderType
|
||||
from openhands.app_server.services.injector import InjectorState
|
||||
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.provider import ProviderHandler, ProviderType
|
||||
from openhands.sdk import TextContent
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.utils.http_session import httpx_verify_option
|
||||
|
||||
JIRA_CLOUD_API_URL = 'https://api.atlassian.com/ex/jira'
|
||||
|
||||
@@ -29,16 +29,16 @@ from storage.jira_dc_integration_store import JiraDcIntegrationStore
|
||||
from storage.jira_dc_user import JiraDcUser
|
||||
from storage.jira_dc_workspace import JiraDcWorkspace
|
||||
|
||||
from openhands.app_server.integrations.provider import ProviderHandler
|
||||
from openhands.app_server.integrations.service_types import Repository
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.provider import ProviderHandler
|
||||
from openhands.integrations.service_types import Repository
|
||||
from openhands.server.shared import server_config
|
||||
from openhands.server.types import (
|
||||
LLMAuthenticationError,
|
||||
MissingSettingsError,
|
||||
SessionExpiredError,
|
||||
)
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.utils.http_session import httpx_verify_option
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from jinja2 import Environment
|
||||
from storage.jira_dc_user import JiraDcUser
|
||||
from storage.jira_dc_workspace import JiraDcWorkspace
|
||||
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
|
||||
|
||||
class JiraDcViewInterface(ABC):
|
||||
|
||||
@@ -30,12 +30,12 @@ from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
ConversationTrigger,
|
||||
)
|
||||
from openhands.app_server.config import get_app_conversation_service
|
||||
from openhands.app_server.integrations.provider import ProviderHandler, ProviderType
|
||||
from openhands.app_server.services.injector import InjectorState
|
||||
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.provider import ProviderHandler, ProviderType
|
||||
from openhands.sdk import TextContent
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
|
||||
integration_store = JiraDcIntegrationStore.get_instance()
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
from uuid import UUID
|
||||
|
||||
from openhands.app_server.integrations.provider import (
|
||||
PROVIDER_TOKEN_TYPE,
|
||||
ProviderHandler,
|
||||
)
|
||||
from openhands.app_server.integrations.service_types import ProviderType, UserGitInfo
|
||||
from openhands.app_server.user.user_context import UserContext
|
||||
from openhands.app_server.user.user_models import UserInfo
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderHandler
|
||||
from openhands.integrations.service_types import ProviderType, UserGitInfo
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
from openhands.sdk.secret import SecretSource, StaticSecret
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
|
||||
|
||||
class ResolverUserContext(UserContext):
|
||||
|
||||
@@ -30,20 +30,20 @@ from sqlalchemy import select
|
||||
from storage.database import a_session_maker
|
||||
from storage.slack_user import SlackUser
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.provider import ProviderHandler
|
||||
from openhands.integrations.service_types import (
|
||||
from openhands.app_server.integrations.provider import ProviderHandler
|
||||
from openhands.app_server.integrations.service_types import (
|
||||
AuthenticationError,
|
||||
ProviderTimeoutError,
|
||||
Repository,
|
||||
)
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.shared import config, server_config, sio
|
||||
from openhands.server.types import (
|
||||
LLMAuthenticationError,
|
||||
MissingSettingsError,
|
||||
SessionExpiredError,
|
||||
)
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
|
||||
authorize_url_generator = AuthorizeUrlGenerator(
|
||||
client_id=SLACK_CLIENT_ID,
|
||||
|
||||
@@ -5,7 +5,7 @@ from integrations.types import SummaryExtractionTracker
|
||||
from jinja2 import Environment
|
||||
from storage.slack_user import SlackUser
|
||||
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -30,13 +30,13 @@ from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
SendMessageRequest,
|
||||
)
|
||||
from openhands.app_server.config import get_app_conversation_service
|
||||
from openhands.app_server.integrations.provider import ProviderHandler
|
||||
from openhands.app_server.sandbox.sandbox_models import SandboxStatus
|
||||
from openhands.app_server.services.injector import InjectorState
|
||||
from openhands.app_server.user.specifiy_user_context import USER_CONTEXT_ATTR
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.provider import ProviderHandler
|
||||
from openhands.sdk import TextContent
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.utils.async_utils import GENERAL_TIMEOUT
|
||||
|
||||
# =================================================
|
||||
|
||||
@@ -3,9 +3,9 @@ from storage.stored_repository import StoredRepository
|
||||
from storage.user_repo_map import UserRepositoryMap
|
||||
from storage.user_repo_map_store import UserRepositoryMapStore
|
||||
|
||||
from openhands.app_server.integrations.service_types import Repository
|
||||
from openhands.core.config.openhands_config import OpenHandsConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.service_types import Repository
|
||||
|
||||
|
||||
async def store_repositories_in_db(repos: list[Repository], user_id: str) -> None:
|
||||
|
||||
@@ -9,8 +9,8 @@ from pydantic import BaseModel
|
||||
if TYPE_CHECKING:
|
||||
from integrations.models import Message
|
||||
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.app_server.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
|
||||
|
||||
class GitLabResourceType(Enum):
|
||||
|
||||
@@ -6,7 +6,7 @@ import re
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from server.constants import WEB_HOST
|
||||
|
||||
from openhands.integrations.service_types import Repository
|
||||
from openhands.app_server.integrations.service_types import Repository
|
||||
|
||||
# ---- DO NOT REMOVE ----
|
||||
# WARNING: Langfuse depends on the WEB_HOST environment variable being set to track events.
|
||||
@@ -65,7 +65,7 @@ def get_user_not_found_message(username: str | None = None) -> str:
|
||||
|
||||
OPENHANDS_RESOLVER_TEMPLATES_DIR = (
|
||||
os.getenv('OPENHANDS_RESOLVER_TEMPLATES_DIR')
|
||||
or 'openhands/integrations/templates/resolver/'
|
||||
or 'openhands/app_server/integrations/templates/resolver/'
|
||||
)
|
||||
_jinja_env = Environment(loader=FileSystemLoader(OPENHANDS_RESOLVER_TEMPLATES_DIR))
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ from pydantic import SecretStr
|
||||
from server.auth.saas_user_auth import SaasUserAuth
|
||||
from server.auth.token_manager import TokenManager
|
||||
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
|
||||
|
||||
def is_budget_exceeded_error(error_message: str) -> bool:
|
||||
|
||||
@@ -40,8 +40,8 @@ from storage.org_member_store import OrgMemberStore
|
||||
from storage.role import Role
|
||||
from storage.role_store import RoleStore
|
||||
|
||||
from openhands.app_server.user_auth import get_user_auth, get_user_id
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.user_auth import get_user_auth, get_user_id
|
||||
|
||||
|
||||
class Permission(str, Enum):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
from openhands.integrations.gitlab.constants import GITLAB_HOST
|
||||
from openhands.app_server.integrations.gitlab.constants import GITLAB_HOST
|
||||
|
||||
GITHUB_APP_CLIENT_ID = os.getenv('GITHUB_APP_CLIENT_ID', '').strip()
|
||||
GITHUB_APP_CLIENT_SECRET = os.getenv('GITHUB_APP_CLIENT_SECRET', '').strip()
|
||||
|
||||
@@ -2,8 +2,8 @@ from integrations.github.github_service import SaaSGitHubService
|
||||
from pydantic import SecretStr
|
||||
from server.auth.auth_utils import user_verifier
|
||||
|
||||
from openhands.app_server.integrations.github.github_types import GitHubUser
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.github.github_types import GitHubUser
|
||||
|
||||
|
||||
def is_user_allowed(user_login: str):
|
||||
|
||||
@@ -3,8 +3,8 @@ import asyncio
|
||||
from pydantic import SecretStr
|
||||
from sqlalchemy import select
|
||||
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ def schedule_gitlab_repo_sync(
|
||||
|
||||
# Lazy import to avoid circular dependency:
|
||||
# middleware -> gitlab_sync -> integrations.gitlab.gitlab_service
|
||||
# -> openhands.integrations.gitlab.gitlab_service -> get_impl
|
||||
# -> openhands.app_server.integrations.gitlab.gitlab_service -> get_impl
|
||||
# -> integrations.gitlab.gitlab_service (circular)
|
||||
from integrations.gitlab.gitlab_service import SaaSGitLabService
|
||||
|
||||
|
||||
@@ -35,15 +35,15 @@ from storage.user_authorization_store import UserAuthorizationStore
|
||||
from storage.user_store import UserStore
|
||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
||||
|
||||
from openhands.app_server.secrets.secrets_models import Secrets
|
||||
from openhands.app_server.settings.settings_models import Settings
|
||||
from openhands.app_server.settings.settings_store import SettingsStore
|
||||
from openhands.integrations.provider import (
|
||||
from openhands.app_server.integrations.provider import (
|
||||
PROVIDER_TOKEN_TYPE,
|
||||
ProviderToken,
|
||||
ProviderType,
|
||||
)
|
||||
from openhands.server.user_auth.user_auth import AuthType, UserAuth
|
||||
from openhands.app_server.secrets.secrets_models import Secrets
|
||||
from openhands.app_server.settings.settings_models import Settings
|
||||
from openhands.app_server.settings.settings_store import SettingsStore
|
||||
from openhands.app_server.user_auth.user_auth import AuthType, UserAuth
|
||||
|
||||
token_manager = TokenManager()
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ from storage.github_app_installation import GithubAppInstallation
|
||||
from storage.offline_token_store import OfflineTokenStore
|
||||
from tenacity import RetryCallState, retry, retry_if_exception_type, stop_after_attempt
|
||||
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.server.types import SessionExpiredError
|
||||
from openhands.utils.http_session import httpx_verify_option
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ from server.auth.constants import (
|
||||
)
|
||||
from server.constants import DEPLOYMENT_MODE
|
||||
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.core.config.utils import load_openhands_config
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.server.config.server_config import ServerConfig
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ Email domain validation utilities for enterprise endpoints.
|
||||
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
|
||||
from openhands.app_server.user_auth import get_user_auth, get_user_id
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.user_auth import get_user_auth, get_user_id
|
||||
|
||||
|
||||
async def get_admin_user_id(
|
||||
|
||||
@@ -15,9 +15,9 @@ from server.auth.saas_user_auth import SaasUserAuth, token_manager
|
||||
from server.routes.auth import set_response_cookie
|
||||
from server.utils.url_utils import get_cookie_domain, get_cookie_samesite
|
||||
|
||||
from openhands.app_server.user_auth.user_auth import AuthType, UserAuth, get_user_auth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.shared import config
|
||||
from openhands.server.user_auth.user_auth import AuthType, UserAuth, get_user_auth
|
||||
|
||||
|
||||
class SetAuthCookieMiddleware:
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.app_server.user.user_models import UserInfo
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
|
||||
|
||||
class SaasUserInfo(UserInfo):
|
||||
|
||||
@@ -12,9 +12,9 @@ from storage.org_member_store import OrgMemberStore
|
||||
from storage.org_service import OrgService
|
||||
from storage.user_store import UserStore
|
||||
|
||||
from openhands.app_server.user_auth import get_user_auth, get_user_id
|
||||
from openhands.app_server.user_auth.user_auth import AuthType
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.user_auth import get_user_auth, get_user_id
|
||||
from openhands.server.user_auth.user_auth import AuthType
|
||||
|
||||
|
||||
# Helper functions for BYOR API key management
|
||||
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
import uuid
|
||||
import warnings
|
||||
from datetime import datetime, timezone
|
||||
from types import MappingProxyType
|
||||
from typing import Annotated, Optional, cast
|
||||
from urllib.parse import quote, urlencode
|
||||
from uuid import UUID as parse_uuid
|
||||
@@ -46,13 +47,16 @@ from storage.database import a_session_maker
|
||||
from storage.user import User
|
||||
from storage.user_store import UserStore
|
||||
|
||||
from openhands.app_server.integrations.provider import (
|
||||
PROVIDER_TOKEN_TYPE,
|
||||
ProviderHandler,
|
||||
ProviderToken,
|
||||
)
|
||||
from openhands.app_server.integrations.service_types import ProviderType, TokenResponse
|
||||
from openhands.app_server.user_auth import get_access_token
|
||||
from openhands.app_server.user_auth.user_auth import get_user_auth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.provider import ProviderHandler
|
||||
from openhands.integrations.service_types import ProviderType, TokenResponse
|
||||
from openhands.server.services.conversation_service import create_provider_tokens_object
|
||||
from openhands.server.shared import config
|
||||
from openhands.server.user_auth import get_access_token
|
||||
from openhands.server.user_auth.user_auth import get_user_auth
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
@@ -63,6 +67,18 @@ oauth_router = APIRouter(prefix='/oauth')
|
||||
token_manager = TokenManager()
|
||||
|
||||
|
||||
def create_provider_tokens_object(
|
||||
providers_set: list[ProviderType],
|
||||
) -> PROVIDER_TOKEN_TYPE:
|
||||
"""Create provider tokens object for the given providers."""
|
||||
provider_information: dict[ProviderType, ProviderToken] = {}
|
||||
|
||||
for provider in providers_set:
|
||||
provider_information[provider] = ProviderToken(token=None, user_id=None)
|
||||
|
||||
return MappingProxyType(provider_information)
|
||||
|
||||
|
||||
def set_response_cookie(
|
||||
request: Request,
|
||||
response: Response,
|
||||
|
||||
@@ -21,7 +21,7 @@ from storage.subscription_access import SubscriptionAccess
|
||||
from storage.user_store import UserStore
|
||||
|
||||
from openhands.app_server.config import get_global_config
|
||||
from openhands.server.user_auth import get_user_id
|
||||
from openhands.app_server.user_auth import get_user_id
|
||||
|
||||
stripe.api_key = STRIPE_API_KEY
|
||||
billing_router = APIRouter(prefix='/api/billing', tags=['Billing'])
|
||||
|
||||
@@ -13,9 +13,9 @@ from server.utils.rate_limit_utils import check_rate_limit_by_user_id
|
||||
from server.utils.url_utils import get_web_url
|
||||
from storage.user_store import UserStore
|
||||
|
||||
from openhands.app_server.user_auth import get_user_id
|
||||
from openhands.app_server.user_auth.user_auth import get_user_auth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.user_auth import get_user_id
|
||||
from openhands.server.user_auth.user_auth import get_user_auth
|
||||
|
||||
# Email validation regex pattern
|
||||
EMAIL_REGEX = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
|
||||
|
||||
@@ -2,25 +2,21 @@ import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request, status
|
||||
from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from integrations.github.data_collector import GitHubDataCollector
|
||||
from integrations.github.github_manager import GithubManager
|
||||
from integrations.models import Message, SourceType
|
||||
from pydantic import BaseModel
|
||||
from server.auth.constants import (
|
||||
AUTOMATION_EVENT_FORWARDING_ENABLED,
|
||||
GITHUB_APP_WEBHOOK_SECRET,
|
||||
)
|
||||
from server.auth.saas_user_auth import SaasUserAuth
|
||||
from server.auth.token_manager import TokenManager
|
||||
from server.services.automation_event_service import AutomationEventService
|
||||
|
||||
from openhands.app_server.integrations.provider import ProviderType
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.provider import ProviderType
|
||||
from openhands.server.user_auth.user_auth import get_user_auth
|
||||
|
||||
# Environment variable to disable GitHub webhooks
|
||||
GITHUB_WEBHOOKS_ENABLED = os.environ.get('GITHUB_WEBHOOKS_ENABLED', '1') in (
|
||||
@@ -109,42 +105,3 @@ async def github_events(
|
||||
except Exception as e:
|
||||
logger.exception(f'Error processing GitHub event: {e}')
|
||||
return JSONResponse(status_code=400, content={'error': 'Invalid payload.'})
|
||||
|
||||
|
||||
class GitHubTokenResponse(BaseModel):
|
||||
"""Response model for the GitHub token endpoint."""
|
||||
|
||||
access_token: str
|
||||
|
||||
|
||||
@github_integration_router.get('/github/token')
|
||||
async def get_github_token(request: Request) -> GitHubTokenResponse:
|
||||
"""Get the GitHub access token for the authenticated user.
|
||||
|
||||
This endpoint retrieves the user's GitHub OAuth token, refreshing it
|
||||
if necessary. The token can be used for GitHub API operations.
|
||||
|
||||
Returns:
|
||||
GitHubTokenResponse containing the access token.
|
||||
|
||||
Raises:
|
||||
HTTPException 401: If the user is not authenticated.
|
||||
HTTPException 404: If no GitHub token is available for the user.
|
||||
"""
|
||||
user_auth = cast(SaasUserAuth, await get_user_auth(request))
|
||||
|
||||
provider_tokens = await user_auth.get_provider_tokens()
|
||||
if not provider_tokens:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail='No provider tokens available for this user.',
|
||||
)
|
||||
|
||||
github_token = provider_tokens.get(ProviderType.GITHUB)
|
||||
if not github_token or not github_token.token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail='No GitHub token available for this user.',
|
||||
)
|
||||
|
||||
return GitHubTokenResponse(access_token=github_token.token.get_secret_value())
|
||||
|
||||
@@ -19,10 +19,10 @@ from server.auth.token_manager import TokenManager
|
||||
from storage.gitlab_webhook import GitlabWebhook
|
||||
from storage.gitlab_webhook_store import GitlabWebhookStore
|
||||
|
||||
from openhands.app_server.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
from openhands.app_server.user_auth import get_user_id
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
from openhands.server.shared import sio
|
||||
from openhands.server.user_auth import get_user_id
|
||||
|
||||
gitlab_integration_router = APIRouter(prefix='/integration')
|
||||
webhook_store = GitlabWebhookStore()
|
||||
|
||||
@@ -20,8 +20,8 @@ from server.auth.token_manager import TokenManager
|
||||
from storage.jira_workspace import JiraWorkspace
|
||||
from storage.redis import create_redis_client
|
||||
|
||||
from openhands.app_server.user_auth.user_auth import get_user_auth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.user_auth.user_auth import get_user_auth
|
||||
|
||||
# Environment variable to disable Jira webhooks
|
||||
JIRA_WEBHOOKS_ENABLED = os.environ.get('JIRA_WEBHOOKS_ENABLED', '0') in (
|
||||
|
||||
@@ -28,8 +28,8 @@ from server.auth.token_manager import TokenManager
|
||||
from server.constants import WEB_HOST
|
||||
from storage.redis import create_redis_client
|
||||
|
||||
from openhands.app_server.user_auth.user_auth import get_user_auth
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.user_auth.user_auth import get_user_auth
|
||||
|
||||
# Environment variable to disable Jira DC webhooks
|
||||
JIRA_DC_WEBHOOKS_ENABLED = os.environ.get('JIRA_DC_WEBHOOKS_ENABLED', '0') in (
|
||||
|
||||
@@ -39,7 +39,10 @@ from storage.slack_team_store import SlackTeamStore
|
||||
from storage.slack_user import SlackUser
|
||||
from storage.user_store import UserStore
|
||||
|
||||
from openhands.integrations.service_types import ProviderTimeoutError, ProviderType
|
||||
from openhands.app_server.integrations.service_types import (
|
||||
ProviderTimeoutError,
|
||||
ProviderType,
|
||||
)
|
||||
from openhands.server.shared import config, sio
|
||||
|
||||
signature_verifier = SignatureVerifier(signing_secret=SLACK_SIGNING_SECRET)
|
||||
|
||||
@@ -10,8 +10,8 @@ from server.utils.url_utils import get_web_url
|
||||
from storage.api_key_store import ApiKeyStore
|
||||
from storage.device_code_store import DeviceCodeStore
|
||||
|
||||
from openhands.app_server.user_auth import get_user_id
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.user_auth import get_user_id
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
|
||||
@@ -22,8 +22,8 @@ from server.utils.rate_limit_utils import check_rate_limit_by_user_id
|
||||
from storage.org_store import OrgStore
|
||||
from storage.role_store import RoleStore
|
||||
|
||||
from openhands.app_server.user_auth import get_user_id
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.user_auth import get_user_id
|
||||
|
||||
# Router for invitation operations on an organization (requires org_id)
|
||||
invitation_router = APIRouter(prefix='/api/organizations/{org_id}/members')
|
||||
|
||||
@@ -50,8 +50,8 @@ from storage.org_service import OrgService
|
||||
from storage.org_store import OrgStore
|
||||
from storage.user_store import UserStore
|
||||
|
||||
from openhands.app_server.user_auth import get_user_id
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.user_auth import get_user_id
|
||||
|
||||
# Initialize API router
|
||||
org_router = APIRouter(prefix='/api/organizations', tags=['Orgs'])
|
||||
|
||||
@@ -17,12 +17,12 @@ from openhands.app_server.config import (
|
||||
depends_user_context,
|
||||
resolve_provider_llm_base_url,
|
||||
)
|
||||
from openhands.app_server.integrations.provider import ProviderHandler
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.app_server.sandbox.session_auth import validate_session_key_ownership
|
||||
from openhands.app_server.user.auth_user_context import AuthUserContext
|
||||
from openhands.app_server.user.user_context import UserContext
|
||||
from openhands.app_server.utils.dependencies import get_dependencies
|
||||
from openhands.integrations.provider import ProviderHandler
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ from server.auth.constants import (
|
||||
)
|
||||
from server.auth.token_manager import TokenManager
|
||||
|
||||
from openhands.app_server.integrations.provider import ProviderType
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.provider import ProviderType
|
||||
from openhands.server.shared import sio
|
||||
|
||||
# Cache TTL constants
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
|
||||
# Simplified imports to avoid dependency chain issues
|
||||
# from openhands.integrations.service_types import ProviderType
|
||||
# from openhands.app_server.integrations.service_types import ProviderType
|
||||
# from openhands.sdk.llm import MetricsSnapshot
|
||||
# from openhands.app_server.app_conversation.app_conversation_models import ConversationTrigger
|
||||
# For now, use Any to avoid import issues
|
||||
|
||||
@@ -30,8 +30,8 @@ from openhands.agent_server.utils import utc_now
|
||||
from openhands.app_server.app_conversation.sql_app_conversation_info_service import (
|
||||
StoredConversationMetadata,
|
||||
)
|
||||
from openhands.app_server.integrations.provider import ProviderType
|
||||
from openhands.app_server.services.injector import InjectorState
|
||||
from openhands.integrations.provider import ProviderType
|
||||
from openhands.sdk.llm import MetricsSnapshot, TokenUsage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -10,8 +10,8 @@ from sqlalchemy.exc import OperationalError
|
||||
from storage.auth_tokens import AuthTokens
|
||||
from storage.database import a_session_maker
|
||||
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
|
||||
# Time buffer (in seconds) before actual expiration to consider token expired
|
||||
# This ensures tokens are refreshed before they actually expire. The
|
||||
|
||||
@@ -6,8 +6,8 @@ from sqlalchemy import and_, desc, select
|
||||
from storage.database import a_session_maker
|
||||
from storage.openhands_pr import OpenhandsPR
|
||||
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
|
||||
|
||||
class OpenhandsPRStore:
|
||||
|
||||
@@ -13,8 +13,8 @@ from sqlalchemy import and_, delete, select, update
|
||||
from storage.database import a_session_maker
|
||||
from storage.proactive_convos import ProactiveConversation
|
||||
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -15,8 +15,8 @@ from storage.database import a_session_maker
|
||||
from storage.gitlab_webhook import GitlabWebhook, WebhookStatus
|
||||
from storage.gitlab_webhook_store import GitlabWebhookStore
|
||||
|
||||
from openhands.app_server.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from integrations.gitlab.gitlab_service import SaaSGitLabService
|
||||
|
||||
@@ -34,7 +34,7 @@ class TestSaaSBitbucketDCServiceInit:
|
||||
def test_refresh_flag_is_true(self):
|
||||
# self.refresh = True is required so the base class BitbucketDCService
|
||||
# retries the request with a refreshed token on 401 responses.
|
||||
# See openhands/integrations/bitbucket_data_center/service/base.py,
|
||||
# See openhands/app_server/integrations/bitbucket_data_center/service/base.py,
|
||||
# which checks `if self.refresh` before attempting the retry.
|
||||
service = SaaSBitbucketDCService()
|
||||
assert service.refresh is True
|
||||
|
||||
@@ -24,7 +24,10 @@ def jinja_env() -> Environment:
|
||||
repo_root = Path(__file__).resolve().parents[5]
|
||||
return Environment(
|
||||
loader=FileSystemLoader(
|
||||
str(repo_root / 'openhands/integrations/templates/resolver/github')
|
||||
str(
|
||||
repo_root
|
||||
/ 'openhands/app_server/integrations/templates/resolver/github'
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ from storage.jira_conversation import JiraConversation
|
||||
from storage.jira_user import JiraUser
|
||||
from storage.jira_workspace import JiraWorkspace
|
||||
|
||||
from openhands.integrations.service_types import ProviderType, Repository
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.app_server.integrations.service_types import ProviderType, Repository
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -16,8 +16,8 @@ from storage.jira_dc_conversation import JiraDcConversation
|
||||
from storage.jira_dc_user import JiraDcUser
|
||||
from storage.jira_dc_workspace import JiraDcWorkspace
|
||||
|
||||
from openhands.integrations.service_types import ProviderType, Repository
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.app_server.integrations.service_types import ProviderType, Repository
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -17,7 +17,7 @@ from integrations.jira_dc.jira_dc_view import (
|
||||
)
|
||||
from integrations.models import Message, SourceType
|
||||
|
||||
from openhands.integrations.service_types import ProviderType, Repository
|
||||
from openhands.app_server.integrations.service_types import ProviderType, Repository
|
||||
from openhands.server.types import (
|
||||
LLMAuthenticationError,
|
||||
MissingSettingsError,
|
||||
|
||||
@@ -17,7 +17,7 @@ from storage.slack_conversation import SlackConversation
|
||||
from storage.slack_user import SlackUser
|
||||
|
||||
from openhands.app_server.sandbox.sandbox_models import SandboxStatus
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
|
||||
@@ -10,11 +10,11 @@ import pytest
|
||||
from pydantic import SecretStr
|
||||
|
||||
from enterprise.integrations.resolver_context import ResolverUserContext
|
||||
from openhands.app_server.secrets.secrets_models import Secrets
|
||||
|
||||
# Import the real classes we want to test
|
||||
from openhands.integrations.provider import CustomSecret, ProviderToken
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.app_server.integrations.provider import CustomSecret, ProviderToken
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.app_server.secrets.secrets_models import Secrets
|
||||
|
||||
# Import the SDK types we need for testing
|
||||
from openhands.sdk.secret import SecretSource, StaticSecret
|
||||
@@ -344,7 +344,7 @@ async def test_get_provider_handler_creates_handler_with_correct_params(
|
||||
handler = await resolver_context._get_provider_handler()
|
||||
|
||||
# Assert
|
||||
from openhands.integrations.provider import ProviderHandler
|
||||
from openhands.app_server.integrations.provider import ProviderHandler
|
||||
|
||||
assert isinstance(handler, ProviderHandler)
|
||||
assert handler.provider_tokens == provider_tokens
|
||||
|
||||
@@ -19,7 +19,7 @@ from server.routes.api_keys import (
|
||||
)
|
||||
from storage.lite_llm_manager import LiteLlmManager
|
||||
|
||||
from openhands.server.user_auth.user_auth import AuthType
|
||||
from openhands.app_server.user_auth.user_auth import AuthType
|
||||
|
||||
|
||||
class TestVerifyByorKeyInLitellm:
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
"""Unit tests for GitHub integration routes.
|
||||
|
||||
Tests for:
|
||||
- get_github_token endpoint
|
||||
"""
|
||||
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from pydantic import SecretStr
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_github_dependencies():
|
||||
"""Mock module-level dependencies before importing the github module.
|
||||
|
||||
The github.py module instantiates GitHubDataCollector at module level,
|
||||
which requires GitHub App credentials. We mock these dependencies to
|
||||
allow importing the module in test environments without credentials.
|
||||
"""
|
||||
# Store original modules if they exist
|
||||
original_modules = {}
|
||||
modules_to_mock = [
|
||||
'integrations.github.data_collector',
|
||||
'integrations.github.github_manager',
|
||||
'server.routes.integration.github',
|
||||
]
|
||||
for mod in modules_to_mock:
|
||||
if mod in sys.modules:
|
||||
original_modules[mod] = sys.modules[mod]
|
||||
del sys.modules[mod]
|
||||
|
||||
# Create mock GitHubDataCollector that doesn't require credentials
|
||||
mock_data_collector_module = MagicMock()
|
||||
mock_data_collector_instance = MagicMock()
|
||||
mock_data_collector_module.GitHubDataCollector.return_value = (
|
||||
mock_data_collector_instance
|
||||
)
|
||||
sys.modules['integrations.github.data_collector'] = mock_data_collector_module
|
||||
|
||||
# Create mock GithubManager
|
||||
mock_github_manager_module = MagicMock()
|
||||
mock_github_manager_instance = MagicMock()
|
||||
mock_github_manager_module.GithubManager.return_value = mock_github_manager_instance
|
||||
sys.modules['integrations.github.github_manager'] = mock_github_manager_module
|
||||
|
||||
yield
|
||||
|
||||
# Clean up the mocked modules
|
||||
for mod in modules_to_mock:
|
||||
if mod in sys.modules:
|
||||
del sys.modules[mod]
|
||||
|
||||
# Restore original modules
|
||||
for mod, original in original_modules.items():
|
||||
sys.modules[mod] = original
|
||||
|
||||
|
||||
class TestGitHubTokenResponse:
|
||||
"""Test suite for GitHubTokenResponse model."""
|
||||
|
||||
def test_github_token_response_with_valid_token(self):
|
||||
"""GitHubTokenResponse should accept a valid access_token."""
|
||||
from server.routes.integration.github import GitHubTokenResponse
|
||||
|
||||
response = GitHubTokenResponse(access_token='ghp_test_token_12345')
|
||||
assert response.access_token == 'ghp_test_token_12345'
|
||||
|
||||
def test_github_token_response_model_dump(self):
|
||||
"""GitHubTokenResponse model_dump should include access_token."""
|
||||
from server.routes.integration.github import GitHubTokenResponse
|
||||
|
||||
response = GitHubTokenResponse(access_token='ghp_test_token_12345')
|
||||
data = response.model_dump()
|
||||
assert data['access_token'] == 'ghp_test_token_12345'
|
||||
|
||||
|
||||
class TestGetGitHubToken:
|
||||
"""Test suite for get_github_token endpoint."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_request(self):
|
||||
"""Create a mock request object."""
|
||||
request = MagicMock()
|
||||
request.state = MagicMock()
|
||||
return request
|
||||
|
||||
@pytest.fixture
|
||||
def mock_saas_user_auth(self):
|
||||
"""Create a mock SaasUserAuth object."""
|
||||
from openhands.integrations.provider import ProviderToken, ProviderType
|
||||
|
||||
mock_auth = AsyncMock()
|
||||
mock_auth.get_provider_tokens = AsyncMock(
|
||||
return_value={
|
||||
ProviderType.GITHUB: ProviderToken(
|
||||
token=SecretStr('ghp_test_token_12345')
|
||||
)
|
||||
}
|
||||
)
|
||||
return mock_auth
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_github_token_success(self, mock_request, mock_saas_user_auth):
|
||||
"""Should return GitHub token when user has a valid token."""
|
||||
from server.routes.integration.github import (
|
||||
GitHubTokenResponse,
|
||||
get_github_token,
|
||||
)
|
||||
|
||||
with patch(
|
||||
'server.routes.integration.github.get_user_auth',
|
||||
return_value=mock_saas_user_auth,
|
||||
):
|
||||
result = await get_github_token(mock_request)
|
||||
|
||||
assert isinstance(result, GitHubTokenResponse)
|
||||
assert result.access_token == 'ghp_test_token_12345'
|
||||
mock_saas_user_auth.get_provider_tokens.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_github_token_no_provider_tokens(self, mock_request):
|
||||
"""Should raise 404 when user has no provider tokens."""
|
||||
from fastapi import HTTPException
|
||||
from server.routes.integration.github import get_github_token
|
||||
|
||||
mock_auth = AsyncMock()
|
||||
mock_auth.get_provider_tokens = AsyncMock(return_value=None)
|
||||
|
||||
with (
|
||||
patch(
|
||||
'server.routes.integration.github.get_user_auth',
|
||||
return_value=mock_auth,
|
||||
),
|
||||
pytest.raises(HTTPException) as exc_info,
|
||||
):
|
||||
await get_github_token(mock_request)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert 'No provider tokens' in exc_info.value.detail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_github_token_no_github_token(self, mock_request):
|
||||
"""Should raise 404 when user has provider tokens but no GitHub token."""
|
||||
from fastapi import HTTPException
|
||||
from server.routes.integration.github import get_github_token
|
||||
|
||||
from openhands.integrations.provider import ProviderToken, ProviderType
|
||||
|
||||
mock_auth = AsyncMock()
|
||||
# Return GitLab token but no GitHub token
|
||||
mock_auth.get_provider_tokens = AsyncMock(
|
||||
return_value={
|
||||
ProviderType.GITLAB: ProviderToken(
|
||||
token=SecretStr('glpat_test_token_12345')
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
'server.routes.integration.github.get_user_auth',
|
||||
return_value=mock_auth,
|
||||
),
|
||||
pytest.raises(HTTPException) as exc_info,
|
||||
):
|
||||
await get_github_token(mock_request)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
assert 'No GitHub token' in exc_info.value.detail
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_github_token_empty_provider_tokens(self, mock_request):
|
||||
"""Should raise 404 when user has empty provider tokens dict."""
|
||||
from fastapi import HTTPException
|
||||
from server.routes.integration.github import get_github_token
|
||||
|
||||
mock_auth = AsyncMock()
|
||||
mock_auth.get_provider_tokens = AsyncMock(return_value={})
|
||||
|
||||
with (
|
||||
patch(
|
||||
'server.routes.integration.github.get_user_auth',
|
||||
return_value=mock_auth,
|
||||
),
|
||||
pytest.raises(HTTPException) as exc_info,
|
||||
):
|
||||
await get_github_token(mock_request)
|
||||
|
||||
assert exc_info.value.status_code == 404
|
||||
# Empty dict is falsy, so it triggers the "no provider tokens" error
|
||||
assert 'No provider tokens' in exc_info.value.detail
|
||||
@@ -22,7 +22,7 @@ from server.routes.orgs import (
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from storage.org_git_claim import OrgGitClaim
|
||||
|
||||
from openhands.server.user_auth import get_user_id
|
||||
from openhands.app_server.user_auth import get_user_id
|
||||
|
||||
TEST_USER_ID = str(uuid.uuid4())
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ from server.routes.orgs import (
|
||||
)
|
||||
from storage.org import Org
|
||||
|
||||
from openhands.app_server.user_auth import get_user_id
|
||||
from openhands.sdk.settings import AgentSettings, ConversationSettings
|
||||
from openhands.server.user_auth import get_user_id
|
||||
|
||||
# Test user ID constant (must be a valid UUID string)
|
||||
TEST_USER_ID = str(uuid.uuid4())
|
||||
|
||||
@@ -16,7 +16,7 @@ from server.routes.user_app_settings_models import (
|
||||
UserNotFoundError,
|
||||
)
|
||||
|
||||
from openhands.server.user_auth import get_user_id
|
||||
from openhands.app_server.user_auth import get_user_id
|
||||
|
||||
TEST_USER_ID = str(uuid.uuid4())
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ class TestGetOrgInfoFromContext:
|
||||
from server.routes.users_v1 import _get_org_info_from_context
|
||||
|
||||
from openhands.app_server.user.auth_user_context import AuthUserContext
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
|
||||
# Create AuthUserContext with a non-SaasUserAuth
|
||||
mock_user_auth = MagicMock(spec=UserAuth)
|
||||
|
||||
@@ -15,7 +15,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
|
||||
# Default patches for constants
|
||||
CONSTANT_PATCHES = {
|
||||
|
||||
@@ -15,7 +15,7 @@ from storage.auth_token_store import (
|
||||
from storage.auth_tokens import AuthTokens
|
||||
from storage.base import Base
|
||||
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -24,8 +24,8 @@ from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
AppConversationInfo,
|
||||
ConversationTrigger,
|
||||
)
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.app_server.user.specifiy_user_context import SpecifyUserContext
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
|
||||
# Test UUIDs
|
||||
USER1_ID = UUID('a1111111-1111-1111-1111-111111111111')
|
||||
|
||||
@@ -13,7 +13,7 @@ from server.auth.auth_error import (
|
||||
from server.auth.saas_user_auth import SaasUserAuth
|
||||
from server.middleware import SetAuthCookieMiddleware
|
||||
|
||||
from openhands.server.user_auth.user_auth import AuthType
|
||||
from openhands.app_server.user_auth.user_auth import AuthType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -19,7 +19,7 @@ from server.routes.auth import (
|
||||
set_response_cookie,
|
||||
)
|
||||
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
|
||||
|
||||
def create_mock_user_authorizer(success: bool = True, error_detail: str | None = None):
|
||||
@@ -799,7 +799,7 @@ async def test_logout_without_refresh_token():
|
||||
|
||||
with patch('server.routes.auth.token_manager') as mock_token_manager:
|
||||
with patch(
|
||||
'openhands.server.user_auth.default_user_auth.DefaultUserAuth.get_instance'
|
||||
'openhands.app_server.user_auth.default_user_auth.DefaultUserAuth.get_instance'
|
||||
) as mock_get_instance:
|
||||
mock_get_instance.side_effect = AuthError()
|
||||
result = await logout(mock_request)
|
||||
|
||||
@@ -68,7 +68,7 @@ class TestAcceptInvitationPostEndpoint:
|
||||
def auth_app(self):
|
||||
"""Create a FastAPI app with dependency overrides for authenticated tests."""
|
||||
|
||||
from openhands.server.user_auth import get_user_id
|
||||
from openhands.app_server.user_auth import get_user_id
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(accept_router)
|
||||
@@ -200,7 +200,7 @@ class TestCreateInvitationBatchEndpoint:
|
||||
@pytest.fixture
|
||||
def batch_app(self):
|
||||
"""Create a FastAPI app with dependency overrides for batch tests."""
|
||||
from openhands.server.user_auth import get_user_id
|
||||
from openhands.app_server.user_auth import get_user_id
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(invitation_router)
|
||||
|
||||
@@ -8,9 +8,9 @@ from pydantic import SecretStr
|
||||
from storage.saas_secrets_store import SaasSecretsStore
|
||||
from storage.stored_custom_secrets import StoredCustomSecrets
|
||||
|
||||
from openhands.app_server.integrations.provider import CustomSecret
|
||||
from openhands.app_server.secrets.secrets_models import Secrets
|
||||
from openhands.core.config.openhands_config import OpenHandsConfig
|
||||
from openhands.integrations.provider import CustomSecret
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -22,8 +22,8 @@ from server.auth.saas_user_auth import (
|
||||
from storage.api_key_store import ApiKeyValidationResult
|
||||
from storage.user_authorization import UserAuthorizationType
|
||||
|
||||
from openhands.app_server.integrations.provider import ProviderToken, ProviderType
|
||||
from openhands.app_server.secrets.secrets_models import Secrets
|
||||
from openhands.integrations.provider import ProviderToken, ProviderType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -21,9 +21,9 @@ from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
from openhands.app_server.app_conversation.sql_app_conversation_info_service import (
|
||||
SQLAppConversationInfoService,
|
||||
)
|
||||
from openhands.app_server.integrations.provider import ProviderType
|
||||
from openhands.app_server.user.specifiy_user_context import SpecifyUserContext
|
||||
from openhands.app_server.utils.sql_utils import Base
|
||||
from openhands.integrations.provider import ProviderType
|
||||
from openhands.sdk.llm import MetricsSnapshot, TokenUsage
|
||||
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ from integrations.slack.slack_manager import (
|
||||
from integrations.slack.slack_view import SlackNewConversationView
|
||||
from storage.slack_user import SlackUser
|
||||
|
||||
from openhands.integrations.service_types import (
|
||||
from openhands.app_server.integrations.service_types import (
|
||||
ProviderTimeoutError,
|
||||
ProviderType,
|
||||
Repository,
|
||||
)
|
||||
from openhands.server.user_auth.user_auth import UserAuth
|
||||
from openhands.app_server.user_auth.user_auth import UserAuth
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
from server.auth.token_manager import TokenManager, create_encryption_utility
|
||||
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -12,9 +12,9 @@ import pytest
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.app_server.integrations.provider import ProviderToken
|
||||
from openhands.app_server.integrations.service_types import ProviderType
|
||||
from openhands.app_server.user.user_context import UserContext
|
||||
from openhands.integrations.provider import ProviderToken
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
|
||||
|
||||
def _make_user_context(provider_tokens, user_id: str = 'user-1') -> UserContext:
|
||||
@@ -97,7 +97,7 @@ async def test_returns_organizations_for_supported_provider(
|
||||
)
|
||||
|
||||
with patch(
|
||||
'openhands.integrations.provider.ProviderHandler.get_service'
|
||||
'openhands.app_server.integrations.provider.ProviderHandler.get_service'
|
||||
) as mock_get_service:
|
||||
mock_service = mock_get_service.return_value
|
||||
setattr(mock_service, service_method, AsyncMock(return_value=service_return))
|
||||
|
||||
@@ -15,11 +15,11 @@ from openhands.agent_server.utils import OpenHandsUUID, utc_now
|
||||
from openhands.app_server.event_callback.event_callback_models import (
|
||||
EventCallbackProcessor,
|
||||
)
|
||||
from openhands.app_server.integrations.service_types import ProviderType, SuggestedTask
|
||||
from openhands.app_server.sandbox.sandbox_models import SandboxStatus
|
||||
|
||||
# Import from new location and re-export for backward compatibility
|
||||
from openhands.app_server.settings.settings_models import SandboxGroupingStrategy
|
||||
from openhands.integrations.service_types import ProviderType, SuggestedTask
|
||||
from openhands.sdk.conversation import ConversationExecutionStatus
|
||||
from openhands.sdk.llm import MetricsSnapshot
|
||||
from openhands.sdk.plugin import PluginSource
|
||||
|
||||
@@ -70,6 +70,8 @@ from openhands.app_server.event_callback.event_callback_service import (
|
||||
from openhands.app_server.event_callback.set_title_callback_processor import (
|
||||
SetTitleCallbackProcessor,
|
||||
)
|
||||
from openhands.app_server.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
|
||||
from openhands.app_server.integrations.service_types import SuggestedTask
|
||||
from openhands.app_server.pending_messages.pending_message_service import (
|
||||
PendingMessageService,
|
||||
)
|
||||
@@ -92,8 +94,6 @@ from openhands.app_server.utils.llm_metadata import (
|
||||
get_llm_metadata,
|
||||
should_set_litellm_extra_body,
|
||||
)
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderType
|
||||
from openhands.integrations.service_types import SuggestedTask
|
||||
from openhands.sdk import Agent, AgentContext, LocalWorkspace
|
||||
from openhands.sdk.agent.acp_agent import ACPAgent
|
||||
from openhands.sdk.hooks import HookConfig
|
||||
|
||||
@@ -15,10 +15,10 @@ import logging
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openhands.app_server.integrations.provider import ProviderType
|
||||
from openhands.app_server.integrations.service_types import AuthenticationError
|
||||
from openhands.app_server.sandbox.sandbox_models import SandboxInfo
|
||||
from openhands.app_server.user.user_context import UserContext
|
||||
from openhands.integrations.provider import ProviderType
|
||||
from openhands.integrations.service_types import AuthenticationError
|
||||
from openhands.sdk.context.skills import KeywordTrigger, Skill, TaskTrigger
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,13 +48,13 @@ from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
AppConversationSortOrder,
|
||||
ConversationTrigger,
|
||||
)
|
||||
from openhands.app_server.integrations.provider import ProviderType
|
||||
from openhands.app_server.services.injector import InjectorState
|
||||
from openhands.app_server.user.user_context import UserContext
|
||||
from openhands.app_server.utils.sql_utils import (
|
||||
Base,
|
||||
create_json_type_decorator,
|
||||
)
|
||||
from openhands.integrations.provider import ProviderType
|
||||
from openhands.sdk import ConversationStats
|
||||
from openhands.sdk.event import ConversationStateUpdateEvent
|
||||
from openhands.sdk.llm import MetricsSnapshot, TokenUsage
|
||||
|
||||
73
openhands/app_server/conversation_paths.py
Normal file
73
openhands/app_server/conversation_paths.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Conversation path helpers for consistent path construction.
|
||||
|
||||
This module provides helper functions for constructing conversation-related
|
||||
storage paths. Use these helpers instead of hardcoding path patterns to ensure
|
||||
consistency across the codebase.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
# The base directory name for v1 conversation storage
|
||||
V1_CONVERSATIONS_DIR = 'v1_conversations'
|
||||
|
||||
|
||||
def get_conversation_dir(conversation_id: UUID | str) -> str:
|
||||
"""Get the conversation directory path segment.
|
||||
|
||||
Args:
|
||||
conversation_id: The conversation ID (UUID or hex string)
|
||||
|
||||
Returns:
|
||||
Path segment like 'v1_conversations/{conversation_id_hex}'
|
||||
|
||||
Example:
|
||||
>>> get_conversation_dir(UUID('12345678-1234-5678-1234-567812345678'))
|
||||
'v1_conversations/12345678123456781234567812345678'
|
||||
>>> get_conversation_dir('12345678123456781234567812345678')
|
||||
'v1_conversations/12345678123456781234567812345678'
|
||||
"""
|
||||
if isinstance(conversation_id, UUID):
|
||||
conversation_id_hex = conversation_id.hex
|
||||
else:
|
||||
# Already a hex string
|
||||
conversation_id_hex = conversation_id
|
||||
return f'{V1_CONVERSATIONS_DIR}/{conversation_id_hex}'
|
||||
|
||||
|
||||
def get_conversation_path(
|
||||
conversation_id: UUID | str,
|
||||
user_id: str | None = None,
|
||||
prefix: Path | str | None = None,
|
||||
) -> Path:
|
||||
"""Get the full conversation path.
|
||||
|
||||
Args:
|
||||
conversation_id: The conversation ID (UUID or hex string)
|
||||
user_id: Optional user ID to include in path
|
||||
prefix: Optional prefix path
|
||||
|
||||
Returns:
|
||||
Full path like '{prefix}/{user_id}/v1_conversations/{conversation_id_hex}'
|
||||
|
||||
Example:
|
||||
>>> get_conversation_path(UUID('...'), user_id='user123', prefix=Path('/data'))
|
||||
Path('/data/user123/v1_conversations/...')
|
||||
"""
|
||||
if isinstance(conversation_id, UUID):
|
||||
conversation_id_hex = conversation_id.hex
|
||||
else:
|
||||
conversation_id_hex = conversation_id
|
||||
|
||||
parts: list[str] = []
|
||||
|
||||
if prefix:
|
||||
parts.append(str(prefix))
|
||||
|
||||
if user_id:
|
||||
parts.append(user_id)
|
||||
|
||||
parts.append(V1_CONVERSATIONS_DIR)
|
||||
parts.append(conversation_id_hex)
|
||||
|
||||
return Path(*parts) if parts else Path(V1_CONVERSATIONS_DIR) / conversation_id_hex
|
||||
@@ -12,6 +12,7 @@ from openhands.app_server.app_conversation.app_conversation_info_service import
|
||||
from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
AppConversationInfo,
|
||||
)
|
||||
from openhands.app_server.conversation_paths import V1_CONVERSATIONS_DIR
|
||||
from openhands.app_server.event.event_service import EventService
|
||||
from openhands.app_server.event_callback.event_callback_models import EventKind
|
||||
from openhands.sdk import Event
|
||||
@@ -60,7 +61,7 @@ class EventServiceBase(EventService, ABC):
|
||||
conversation_info = await task
|
||||
if conversation_info and conversation_info.created_by_user_id:
|
||||
path /= conversation_info.created_by_user_id
|
||||
path = path / 'v1_conversations' / conversation_id.hex
|
||||
path = path / V1_CONVERSATIONS_DIR / conversation_id.hex
|
||||
return path
|
||||
|
||||
async def get_event(self, conversation_id: UUID, event_id: UUID) -> Event | None:
|
||||
|
||||
@@ -34,6 +34,7 @@ from openhands.app_server.event_callback.event_callback_models import EventCallb
|
||||
from openhands.app_server.event_callback.set_title_callback_processor import (
|
||||
SetTitleCallbackProcessor,
|
||||
)
|
||||
from openhands.app_server.integrations.provider import ProviderType
|
||||
from openhands.app_server.sandbox.sandbox_models import SandboxInfo
|
||||
from openhands.app_server.services.injector import InjectorState
|
||||
from openhands.app_server.services.jwt_service import JwtService
|
||||
@@ -43,14 +44,13 @@ from openhands.app_server.user.specifiy_user_context import (
|
||||
USER_CONTEXT_ATTR,
|
||||
SpecifyUserContext,
|
||||
)
|
||||
from openhands.integrations.provider import ProviderType
|
||||
from openhands.app_server.user_auth.default_user_auth import DefaultUserAuth
|
||||
from openhands.app_server.user_auth.user_auth import (
|
||||
get_for_user as get_user_auth_for_user,
|
||||
)
|
||||
from openhands.sdk import ConversationExecutionStatus, Event
|
||||
from openhands.sdk.event import ConversationStateUpdateEvent
|
||||
from openhands.server.types import AppMode
|
||||
from openhands.server.user_auth.default_user_auth import DefaultUserAuth
|
||||
from openhands.server.user_auth.user_auth import (
|
||||
get_for_user as get_user_auth_for_user,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix='/webhooks', tags=['Webhooks'])
|
||||
event_service_dependency = depends_event_service()
|
||||
|
||||
77
openhands/app_server/file_store/README.md
Normal file
77
openhands/app_server/file_store/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# OpenHands FileStore Module
|
||||
|
||||
The file store module provides different storage backends for file operations in OpenHands. This module implements a common interface (`FileStore`) that allows for interchangeable storage backends.
|
||||
|
||||
All FileStore implementations use `DiscriminatedUnionMixin` for automatic serialization/deserialization with a `kind` discriminator field based on the class name.
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from openhands.app_server.file_store import get_file_store, LocalFileStore
|
||||
|
||||
# Using the factory function
|
||||
store = get_file_store("local", "/tmp/file_store")
|
||||
|
||||
# Or instantiate directly
|
||||
store = LocalFileStore(root="/tmp/file_store")
|
||||
|
||||
# Write, read, list, and delete operations
|
||||
store.write("example.txt", "Hello, world!")
|
||||
content = store.read("example.txt")
|
||||
files = store.list("/")
|
||||
store.delete("example.txt")
|
||||
```
|
||||
|
||||
## Available Storage Backends
|
||||
|
||||
### 1. Local File Storage (`LocalFileStore`)
|
||||
|
||||
Local file storage saves files to the local filesystem.
|
||||
|
||||
**Parameters:**
|
||||
- `root`: The root directory for file storage (supports `~` expansion)
|
||||
|
||||
### 2. In-Memory Storage (`InMemoryFileStore`)
|
||||
|
||||
In-memory storage keeps files in memory, useful for testing or temporary storage.
|
||||
|
||||
**Parameters:**
|
||||
- `files`: Optional dictionary of initial files (default: empty)
|
||||
|
||||
### 3. Amazon S3 Storage (`S3FileStore`)
|
||||
|
||||
S3 storage uses Amazon S3 or compatible services for file storage.
|
||||
|
||||
**Parameters:**
|
||||
- `bucket`: The S3 bucket name (falls back to `AWS_S3_BUCKET` environment variable if empty)
|
||||
|
||||
**Environment Variables:**
|
||||
- `AWS_ACCESS_KEY_ID`: Your AWS access key
|
||||
- `AWS_SECRET_ACCESS_KEY`: Your AWS secret key
|
||||
- `AWS_S3_BUCKET`: Default bucket name (used if `bucket` parameter is empty)
|
||||
- `AWS_S3_ENDPOINT`: Optional custom endpoint for S3-compatible services
|
||||
- `AWS_S3_SECURE`: Whether to use HTTPS (default: "true")
|
||||
|
||||
### 4. Google Cloud Storage (`GoogleCloudFileStore`)
|
||||
|
||||
Google Cloud Storage uses GCS buckets for file storage.
|
||||
|
||||
**Parameters:**
|
||||
- `bucket_name`: The GCS bucket name (falls back to `GOOGLE_CLOUD_BUCKET_NAME` environment variable if empty)
|
||||
|
||||
**Environment Variables:**
|
||||
- `GOOGLE_CLOUD_BUCKET_NAME`: Default bucket name (used if `bucket_name` parameter is empty)
|
||||
- `GOOGLE_APPLICATION_CREDENTIALS`: Path to Google Cloud credentials JSON file
|
||||
|
||||
## Configuration
|
||||
|
||||
To configure the file store in OpenHands, use the following configuration options:
|
||||
|
||||
```toml
|
||||
[core]
|
||||
# File store type: "local", "memory", "s3", "google_cloud"
|
||||
file_store = "local"
|
||||
|
||||
# Path/bucket for file store (interpretation depends on file_store type)
|
||||
file_store_path = "/tmp/file_store"
|
||||
```
|
||||
21
openhands/app_server/file_store/__init__.py
Normal file
21
openhands/app_server/file_store/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from openhands.app_server.file_store.files import FileStore
|
||||
from openhands.app_server.file_store.google_cloud import GoogleCloudFileStore
|
||||
from openhands.app_server.file_store.local import LocalFileStore
|
||||
from openhands.app_server.file_store.memory import InMemoryFileStore
|
||||
from openhands.app_server.file_store.s3 import S3FileStore
|
||||
|
||||
|
||||
def get_file_store(
|
||||
file_store_type: str,
|
||||
file_store_path: str | None = None,
|
||||
) -> FileStore:
|
||||
if file_store_type == 'local':
|
||||
if file_store_path is None:
|
||||
raise ValueError('file_store_path is required for local file store')
|
||||
return LocalFileStore(root=file_store_path)
|
||||
elif file_store_type == 's3':
|
||||
return S3FileStore(bucket_name=file_store_path or '')
|
||||
elif file_store_type == 'google_cloud':
|
||||
return GoogleCloudFileStore(bucket_name=file_store_path or '')
|
||||
else:
|
||||
return InMemoryFileStore()
|
||||
30
openhands/app_server/file_store/files.py
Normal file
30
openhands/app_server/file_store/files.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from pydantic import ConfigDict
|
||||
|
||||
from openhands.sdk.utils.models import DiscriminatedUnionMixin
|
||||
|
||||
|
||||
class FileStore(DiscriminatedUnionMixin, ABC):
|
||||
"""Base class for file storage implementations.
|
||||
|
||||
Uses DiscriminatedUnionMixin for automatic `kind` field based on class name.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra='forbid', arbitrary_types_allowed=True)
|
||||
|
||||
@abstractmethod
|
||||
def write(self, path: str, contents: str | bytes) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def read(self, path: str) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def list(self, path: str) -> list[str]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, path: str) -> None:
|
||||
pass
|
||||
@@ -5,21 +5,44 @@ from google.cloud import storage
|
||||
from google.cloud.storage.blob import Blob
|
||||
from google.cloud.storage.bucket import Bucket
|
||||
from google.cloud.storage.client import Client
|
||||
from pydantic import Field, PrivateAttr
|
||||
|
||||
from openhands.storage.files import FileStore
|
||||
from openhands.app_server.file_store.files import FileStore
|
||||
|
||||
|
||||
class GoogleCloudFileStore(FileStore):
|
||||
def __init__(self, bucket_name: str | None = None) -> None:
|
||||
"""Create a new FileStore.
|
||||
"""Google Cloud Storage file store.
|
||||
|
||||
If GOOGLE_APPLICATION_CREDENTIALS is defined in the environment it will be used
|
||||
for authentication. Otherwise access will be anonymous.
|
||||
"""
|
||||
if bucket_name is None:
|
||||
bucket_name = os.environ['GOOGLE_CLOUD_BUCKET_NAME']
|
||||
self.storage_client: Client = storage.Client()
|
||||
self.bucket: Bucket = self.storage_client.bucket(bucket_name)
|
||||
If GOOGLE_APPLICATION_CREDENTIALS is defined in the environment it will be used
|
||||
for authentication. Otherwise access will be anonymous.
|
||||
|
||||
The storage client and bucket are initialized lazily on first access.
|
||||
"""
|
||||
|
||||
bucket_name: str = Field(default='')
|
||||
|
||||
_storage_client: Client | None = PrivateAttr(default=None)
|
||||
_bucket: Bucket | None = PrivateAttr(default=None)
|
||||
|
||||
def _get_bucket_name(self) -> str:
|
||||
"""Get bucket name, falling back to environment variable if not set."""
|
||||
if self.bucket_name:
|
||||
return self.bucket_name
|
||||
return os.environ['GOOGLE_CLOUD_BUCKET_NAME']
|
||||
|
||||
@property
|
||||
def storage_client(self) -> Client:
|
||||
"""Get the storage client, initializing lazily on first access."""
|
||||
if self._storage_client is None:
|
||||
self._storage_client = storage.Client()
|
||||
return self._storage_client
|
||||
|
||||
@property
|
||||
def bucket(self) -> Bucket:
|
||||
"""Get the bucket, initializing lazily on first access."""
|
||||
if self._bucket is None:
|
||||
self._bucket = self.storage_client.bucket(self._get_bucket_name())
|
||||
return self._bucket
|
||||
|
||||
def write(self, path: str, contents: str | bytes) -> None:
|
||||
blob: Blob = self.bucket.blob(path)
|
||||
@@ -2,18 +2,21 @@ import os
|
||||
import shutil
|
||||
import threading
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
from openhands.app_server.file_store.files import FileStore
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.storage.files import FileStore
|
||||
|
||||
|
||||
class LocalFileStore(FileStore):
|
||||
root: str
|
||||
|
||||
def __init__(self, root: str):
|
||||
if root.startswith('~'):
|
||||
root = os.path.expanduser(root)
|
||||
self.root = root
|
||||
@model_validator(mode='after')
|
||||
def _setup_root(self) -> 'LocalFileStore':
|
||||
if self.root.startswith('~'):
|
||||
self.root = os.path.expanduser(self.root)
|
||||
os.makedirs(self.root, exist_ok=True)
|
||||
return self
|
||||
|
||||
def get_full_path(self, path: str) -> str:
|
||||
if path.startswith('/'):
|
||||
@@ -1,16 +1,13 @@
|
||||
import os
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from openhands.app_server.file_store.files import FileStore
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.storage.files import FileStore
|
||||
|
||||
|
||||
class InMemoryFileStore(FileStore):
|
||||
files: dict[str, str]
|
||||
|
||||
def __init__(self, files: dict[str, str] | None = None) -> None:
|
||||
self.files = {}
|
||||
if files is not None:
|
||||
self.files = files
|
||||
files: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
def write(self, path: str, contents: str | bytes) -> None:
|
||||
if isinstance(contents, bytes):
|
||||
@@ -3,8 +3,9 @@ from typing import Any, TypedDict
|
||||
|
||||
import boto3
|
||||
import botocore
|
||||
from pydantic import Field, PrivateAttr
|
||||
|
||||
from openhands.storage.files import FileStore
|
||||
from openhands.app_server.file_store.files import FileStore
|
||||
|
||||
|
||||
class S3ObjectDict(TypedDict):
|
||||
@@ -20,45 +21,64 @@ class ListObjectsV2OutputDict(TypedDict):
|
||||
|
||||
|
||||
class S3FileStore(FileStore):
|
||||
def __init__(self, bucket_name: str | None) -> None:
|
||||
access_key = os.getenv('AWS_ACCESS_KEY_ID')
|
||||
secret_key = os.getenv('AWS_SECRET_ACCESS_KEY')
|
||||
secure = os.getenv('AWS_S3_SECURE', 'true').lower() == 'true'
|
||||
endpoint = self._ensure_url_scheme(secure, os.getenv('AWS_S3_ENDPOINT'))
|
||||
if bucket_name is None:
|
||||
bucket_name = os.environ['AWS_S3_BUCKET']
|
||||
self.bucket: str = bucket_name
|
||||
self.client: Any = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=access_key,
|
||||
aws_secret_access_key=secret_key,
|
||||
endpoint_url=endpoint,
|
||||
use_ssl=secure,
|
||||
)
|
||||
"""S3-compatible file store.
|
||||
|
||||
The S3 client is initialized lazily on first access.
|
||||
"""
|
||||
|
||||
bucket_name: str = Field(default='')
|
||||
|
||||
_client: Any = PrivateAttr(default=None)
|
||||
_resolved_bucket: str | None = PrivateAttr(default=None)
|
||||
|
||||
def _get_bucket_name(self) -> str:
|
||||
"""Get bucket name, falling back to environment variable if not set."""
|
||||
if self._resolved_bucket is None:
|
||||
self._resolved_bucket = self.bucket_name or os.environ['AWS_S3_BUCKET']
|
||||
return self._resolved_bucket
|
||||
|
||||
@property
|
||||
def client(self) -> Any:
|
||||
"""Get the S3 client, initializing lazily on first access."""
|
||||
if self._client is None:
|
||||
access_key = os.getenv('AWS_ACCESS_KEY_ID')
|
||||
secret_key = os.getenv('AWS_SECRET_ACCESS_KEY')
|
||||
secure = os.getenv('AWS_S3_SECURE', 'true').lower() == 'true'
|
||||
endpoint = self._ensure_url_scheme(secure, os.getenv('AWS_S3_ENDPOINT'))
|
||||
self._client = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=access_key,
|
||||
aws_secret_access_key=secret_key,
|
||||
endpoint_url=endpoint,
|
||||
use_ssl=secure,
|
||||
)
|
||||
return self._client
|
||||
|
||||
def write(self, path: str, contents: str | bytes) -> None:
|
||||
try:
|
||||
as_bytes = (
|
||||
contents.encode('utf-8') if isinstance(contents, str) else contents
|
||||
)
|
||||
self.client.put_object(Bucket=self.bucket, Key=path, Body=as_bytes)
|
||||
self.client.put_object(
|
||||
Bucket=self._get_bucket_name(), Key=path, Body=as_bytes
|
||||
)
|
||||
except botocore.exceptions.ClientError as e:
|
||||
if e.response['Error']['Code'] == 'AccessDenied':
|
||||
raise FileNotFoundError(
|
||||
f"Error: Access denied to bucket '{self.bucket}'."
|
||||
f"Error: Access denied to bucket '{self._get_bucket_name()}'."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self.bucket}' does not exist."
|
||||
f"Error: The bucket '{self._get_bucket_name()}' does not exist."
|
||||
)
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to write to bucket '{self.bucket}' at path {path}: {e}"
|
||||
f"Error: Failed to write to bucket '{self._get_bucket_name()}' at path {path}: {e}"
|
||||
)
|
||||
|
||||
def read(self, path: str) -> str:
|
||||
try:
|
||||
response: GetObjectOutputDict = self.client.get_object(
|
||||
Bucket=self.bucket, Key=path
|
||||
Bucket=self._get_bucket_name(), Key=path
|
||||
)
|
||||
with response['Body'] as stream:
|
||||
return str(stream.read().decode('utf-8'))
|
||||
@@ -66,19 +86,19 @@ class S3FileStore(FileStore):
|
||||
# Catch all S3-related errors
|
||||
if e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self.bucket}' does not exist."
|
||||
f"Error: The bucket '{self._get_bucket_name()}' does not exist."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'NoSuchKey':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The object key '{path}' does not exist in bucket '{self.bucket}'."
|
||||
f"Error: The object key '{path}' does not exist in bucket '{self._get_bucket_name()}'."
|
||||
)
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
|
||||
f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to read from bucket '{self.bucket}' at path {path}: {e}"
|
||||
f"Error: Failed to read from bucket '{self._get_bucket_name()}' at path {path}: {e}"
|
||||
)
|
||||
|
||||
def list(self, path: str) -> list[str]:
|
||||
@@ -96,7 +116,7 @@ class S3FileStore(FileStore):
|
||||
results: set[str] = set()
|
||||
prefix_len = len(path)
|
||||
response: ListObjectsV2OutputDict = self.client.list_objects_v2(
|
||||
Bucket=self.bucket, Prefix=path
|
||||
Bucket=self._get_bucket_name(), Prefix=path
|
||||
)
|
||||
contents = response.get('Contents')
|
||||
if not contents:
|
||||
@@ -123,34 +143,36 @@ class S3FileStore(FileStore):
|
||||
|
||||
# Try to delete any child resources (Assume the path is a directory)
|
||||
response = self.client.list_objects_v2(
|
||||
Bucket=self.bucket, Prefix=f'{path}/'
|
||||
Bucket=self._get_bucket_name(), Prefix=f'{path}/'
|
||||
)
|
||||
for content in response.get('Contents') or []:
|
||||
self.client.delete_object(Bucket=self.bucket, Key=content['Key'])
|
||||
self.client.delete_object(
|
||||
Bucket=self._get_bucket_name(), Key=content['Key']
|
||||
)
|
||||
|
||||
# Next try to delete item as a file
|
||||
self.client.delete_object(Bucket=self.bucket, Key=path)
|
||||
self.client.delete_object(Bucket=self._get_bucket_name(), Key=path)
|
||||
|
||||
except botocore.exceptions.ClientError as e:
|
||||
if e.response['Error']['Code'] == 'NoSuchBucket':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The bucket '{self.bucket}' does not exist."
|
||||
f"Error: The bucket '{self._get_bucket_name()}' does not exist."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'AccessDenied':
|
||||
raise FileNotFoundError(
|
||||
f"Error: Access denied to bucket '{self.bucket}'."
|
||||
f"Error: Access denied to bucket '{self._get_bucket_name()}'."
|
||||
)
|
||||
elif e.response['Error']['Code'] == 'NoSuchKey':
|
||||
raise FileNotFoundError(
|
||||
f"Error: The object key '{path}' does not exist in bucket '{self.bucket}'."
|
||||
f"Error: The object key '{path}' does not exist in bucket '{self._get_bucket_name()}'."
|
||||
)
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self.bucket}': {e}"
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}': {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise FileNotFoundError(
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self.bucket}: {e}"
|
||||
f"Error: Failed to delete key '{path}' from bucket '{self._get_bucket_name()}: {e}"
|
||||
)
|
||||
|
||||
def _ensure_url_scheme(self, secure: bool, url: str | None) -> str | None:
|
||||
@@ -4,7 +4,7 @@ from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openhands.integrations.service_types import (
|
||||
from openhands.app_server.integrations.service_types import (
|
||||
Branch,
|
||||
Repository,
|
||||
SuggestedTask,
|
||||
|
||||
@@ -18,6 +18,13 @@ from openhands.app_server.git.git_models import (
|
||||
SortOrder,
|
||||
SuggestedTaskPage,
|
||||
)
|
||||
from openhands.app_server.integrations.provider import ProviderHandler
|
||||
from openhands.app_server.integrations.service_types import (
|
||||
Branch,
|
||||
ProviderType,
|
||||
Repository,
|
||||
SuggestedTask,
|
||||
)
|
||||
from openhands.app_server.user.user_context import UserContext
|
||||
from openhands.app_server.utils.dependencies import get_dependencies
|
||||
from openhands.app_server.utils.paging_utils import (
|
||||
@@ -25,16 +32,9 @@ from openhands.app_server.utils.paging_utils import (
|
||||
encode_page_id,
|
||||
paginate_results,
|
||||
)
|
||||
from openhands.integrations.provider import ProviderHandler
|
||||
from openhands.integrations.service_types import (
|
||||
Branch,
|
||||
ProviderType,
|
||||
Repository,
|
||||
SuggestedTask,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.app_server.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
|
||||
# We use the get_dependencies method here to signal to the OpenAPI docs that this endpoint
|
||||
# is protected. The actual protection is provided by SetAuthCookieMiddleware
|
||||
|
||||
@@ -4,22 +4,26 @@ from typing import Any
|
||||
import httpx
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.integrations.azure_devops.service.branches import (
|
||||
from openhands.app_server.integrations.azure_devops.service.branches import (
|
||||
AzureDevOpsBranchesMixin,
|
||||
)
|
||||
from openhands.integrations.azure_devops.service.features import (
|
||||
from openhands.app_server.integrations.azure_devops.service.features import (
|
||||
AzureDevOpsFeaturesMixin,
|
||||
)
|
||||
from openhands.integrations.azure_devops.service.prs import AzureDevOpsPRsMixin
|
||||
from openhands.integrations.azure_devops.service.repos import AzureDevOpsReposMixin
|
||||
from openhands.integrations.azure_devops.service.resolver import (
|
||||
from openhands.app_server.integrations.azure_devops.service.prs import (
|
||||
AzureDevOpsPRsMixin,
|
||||
)
|
||||
from openhands.app_server.integrations.azure_devops.service.repos import (
|
||||
AzureDevOpsReposMixin,
|
||||
)
|
||||
from openhands.app_server.integrations.azure_devops.service.resolver import (
|
||||
AzureDevOpsResolverMixin,
|
||||
)
|
||||
from openhands.integrations.azure_devops.service.work_items import (
|
||||
from openhands.app_server.integrations.azure_devops.service.work_items import (
|
||||
AzureDevOpsWorkItemsMixin,
|
||||
)
|
||||
from openhands.integrations.protocols.http_client import HTTPClient
|
||||
from openhands.integrations.service_types import (
|
||||
from openhands.app_server.integrations.protocols.http_client import HTTPClient
|
||||
from openhands.app_server.integrations.service_types import (
|
||||
BaseGitService,
|
||||
GitService,
|
||||
ProviderType,
|
||||
@@ -242,7 +246,7 @@ class AzureDevOpsService(
|
||||
# Dynamic class loading to support custom implementations (e.g., SaaS)
|
||||
azure_devops_service_cls = os.environ.get(
|
||||
'OPENHANDS_AZURE_DEVOPS_SERVICE_CLS',
|
||||
'openhands.integrations.azure_devops.azure_devops_service.AzureDevOpsService',
|
||||
'openhands.app_server.integrations.azure_devops.azure_devops_service.AzureDevOpsService',
|
||||
)
|
||||
|
||||
# Lazy loading to avoid circular imports
|
||||
@@ -4,8 +4,8 @@ from urllib.parse import quote
|
||||
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.integrations.protocols.http_client import HTTPClient
|
||||
from openhands.integrations.service_types import (
|
||||
from openhands.app_server.integrations.protocols.http_client import HTTPClient
|
||||
from openhands.app_server.integrations.service_types import (
|
||||
BaseGitService,
|
||||
RequestMethod,
|
||||
)
|
||||
@@ -1,7 +1,12 @@
|
||||
"""Branch operations for Azure DevOps integration."""
|
||||
|
||||
from openhands.integrations.azure_devops.service.base import AzureDevOpsMixinBase
|
||||
from openhands.integrations.service_types import Branch, PaginatedBranchesResponse
|
||||
from openhands.app_server.integrations.azure_devops.service.base import (
|
||||
AzureDevOpsMixinBase,
|
||||
)
|
||||
from openhands.app_server.integrations.service_types import (
|
||||
Branch,
|
||||
PaginatedBranchesResponse,
|
||||
)
|
||||
|
||||
|
||||
class AzureDevOpsBranchesMixin(AzureDevOpsMixinBase):
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Feature operations for Azure DevOps integration (microagents, suggested tasks, user)."""
|
||||
|
||||
from openhands.integrations.azure_devops.service.base import AzureDevOpsMixinBase
|
||||
from openhands.integrations.service_types import (
|
||||
from openhands.app_server.integrations.azure_devops.service.base import (
|
||||
AzureDevOpsMixinBase,
|
||||
)
|
||||
from openhands.app_server.integrations.service_types import (
|
||||
ProviderType,
|
||||
RequestMethod,
|
||||
SuggestedTask,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user