indent 0.1.26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exponent/__init__.py +34 -0
- exponent/cli.py +110 -0
- exponent/commands/cloud_commands.py +585 -0
- exponent/commands/common.py +411 -0
- exponent/commands/config_commands.py +334 -0
- exponent/commands/run_commands.py +222 -0
- exponent/commands/settings.py +56 -0
- exponent/commands/types.py +111 -0
- exponent/commands/upgrade.py +29 -0
- exponent/commands/utils.py +146 -0
- exponent/core/config.py +180 -0
- exponent/core/graphql/__init__.py +0 -0
- exponent/core/graphql/client.py +61 -0
- exponent/core/graphql/get_chats_query.py +47 -0
- exponent/core/graphql/mutations.py +160 -0
- exponent/core/graphql/queries.py +146 -0
- exponent/core/graphql/subscriptions.py +16 -0
- exponent/core/remote_execution/checkpoints.py +212 -0
- exponent/core/remote_execution/cli_rpc_types.py +499 -0
- exponent/core/remote_execution/client.py +999 -0
- exponent/core/remote_execution/code_execution.py +77 -0
- exponent/core/remote_execution/default_env.py +31 -0
- exponent/core/remote_execution/error_info.py +45 -0
- exponent/core/remote_execution/exceptions.py +10 -0
- exponent/core/remote_execution/file_write.py +35 -0
- exponent/core/remote_execution/files.py +330 -0
- exponent/core/remote_execution/git.py +268 -0
- exponent/core/remote_execution/http_fetch.py +94 -0
- exponent/core/remote_execution/languages/python_execution.py +239 -0
- exponent/core/remote_execution/languages/shell_streaming.py +226 -0
- exponent/core/remote_execution/languages/types.py +20 -0
- exponent/core/remote_execution/port_utils.py +73 -0
- exponent/core/remote_execution/session.py +128 -0
- exponent/core/remote_execution/system_context.py +26 -0
- exponent/core/remote_execution/terminal_session.py +375 -0
- exponent/core/remote_execution/terminal_types.py +29 -0
- exponent/core/remote_execution/tool_execution.py +595 -0
- exponent/core/remote_execution/tool_type_utils.py +39 -0
- exponent/core/remote_execution/truncation.py +296 -0
- exponent/core/remote_execution/types.py +635 -0
- exponent/core/remote_execution/utils.py +477 -0
- exponent/core/types/__init__.py +0 -0
- exponent/core/types/command_data.py +206 -0
- exponent/core/types/event_types.py +89 -0
- exponent/core/types/generated/__init__.py +0 -0
- exponent/core/types/generated/strategy_info.py +213 -0
- exponent/migration-docs/login.md +112 -0
- exponent/py.typed +4 -0
- exponent/utils/__init__.py +0 -0
- exponent/utils/colors.py +92 -0
- exponent/utils/version.py +289 -0
- indent-0.1.26.dist-info/METADATA +38 -0
- indent-0.1.26.dist-info/RECORD +55 -0
- indent-0.1.26.dist-info/WHEEL +4 -0
- indent-0.1.26.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from collections.abc import Callable, Sequence
|
|
2
|
+
from gettext import gettext
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import questionary
|
|
7
|
+
|
|
8
|
+
from exponent.core.types.generated.strategy_info import StrategyInfo
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AutoCompleteOption(click.Option):
|
|
12
|
+
prompt: str
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
param_decls: Sequence[str] | None = None,
|
|
17
|
+
prompt: bool | str = True,
|
|
18
|
+
choices: list[str] | None = None,
|
|
19
|
+
**kwargs: Any,
|
|
20
|
+
):
|
|
21
|
+
super().__init__(param_decls, prompt=prompt, **kwargs)
|
|
22
|
+
if isinstance(self.type, click.Choice):
|
|
23
|
+
self.choices = self.type.choices
|
|
24
|
+
else:
|
|
25
|
+
self.choices = choices or []
|
|
26
|
+
|
|
27
|
+
def prompt_for_value(self, ctx: click.core.Context) -> Any:
|
|
28
|
+
return questionary.autocomplete(
|
|
29
|
+
self.prompt,
|
|
30
|
+
list(self.choices),
|
|
31
|
+
style=questionary.Style(
|
|
32
|
+
[
|
|
33
|
+
("question", "bold"), # question text
|
|
34
|
+
(
|
|
35
|
+
"answer",
|
|
36
|
+
"fg:#33ccff bold",
|
|
37
|
+
), # submitted answer text behind the question
|
|
38
|
+
(
|
|
39
|
+
"answer",
|
|
40
|
+
"bg:#000066",
|
|
41
|
+
), # submitted answer text behind the question
|
|
42
|
+
]
|
|
43
|
+
),
|
|
44
|
+
).unsafe_ask()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StrategyChoice(click.Choice[str]):
|
|
48
|
+
def __init__(self, choices: Sequence[StrategyInfo]) -> None:
|
|
49
|
+
self.strategy_choices = choices
|
|
50
|
+
self.choices = [strategy.strategy_name.value for strategy in choices]
|
|
51
|
+
self.case_sensitive = True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StrategyOption(AutoCompleteOption):
|
|
55
|
+
def __init__(self, *args: Any, type: StrategyChoice, **kwargs: Any):
|
|
56
|
+
super().__init__(*args, type=type, **kwargs)
|
|
57
|
+
self.default = self.default_choice(type.strategy_choices)
|
|
58
|
+
self.strategy_choices = type.strategy_choices
|
|
59
|
+
|
|
60
|
+
def _format_strategy_info(
|
|
61
|
+
self, strategy_info: StrategyInfo, formatter: click.HelpFormatter
|
|
62
|
+
) -> None:
|
|
63
|
+
row = (strategy_info.strategy_name.value, strategy_info.display_name)
|
|
64
|
+
formatter.write_dl([row])
|
|
65
|
+
with formatter.indentation():
|
|
66
|
+
formatter.write_text(strategy_info.description)
|
|
67
|
+
|
|
68
|
+
def help_extra_hook(self, formatter: click.HelpFormatter) -> None:
|
|
69
|
+
with formatter.section("Strategies"):
|
|
70
|
+
for strategy_info in self.strategy_choices:
|
|
71
|
+
formatter.write_paragraph()
|
|
72
|
+
self._format_strategy_info(strategy_info, formatter)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def default_choice(choices: Sequence[StrategyInfo]) -> str:
|
|
76
|
+
return min(choices, key=lambda x: x.display_order).strategy_name.value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ExponentCommand(click.Command):
|
|
80
|
+
def format_options(
|
|
81
|
+
self, ctx: click.Context, formatter: click.HelpFormatter
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Writes all the options into the formatter if they exist."""
|
|
84
|
+
opts = []
|
|
85
|
+
for param in self.get_params(ctx):
|
|
86
|
+
rv = param.get_help_record(ctx)
|
|
87
|
+
hook = getattr(param, "help_extra_hook", None)
|
|
88
|
+
if rv is not None:
|
|
89
|
+
opts.append((rv, hook))
|
|
90
|
+
|
|
91
|
+
if not opts:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
with formatter.section(gettext("Options")):
|
|
95
|
+
for opt, hook in opts:
|
|
96
|
+
formatter.write_dl([opt])
|
|
97
|
+
if hook is not None:
|
|
98
|
+
hook(formatter)
|
|
99
|
+
formatter.write_paragraph()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ExponentGroup(click.Group):
|
|
103
|
+
command_class = ExponentCommand
|
|
104
|
+
group_class = type
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def exponent_cli_group(
|
|
108
|
+
name: str | None = None,
|
|
109
|
+
**attrs: Any,
|
|
110
|
+
) -> Callable[[Callable[..., Any]], ExponentGroup]:
|
|
111
|
+
return click.command(name, ExponentGroup, **attrs)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from exponent.commands.types import exponent_cli_group
|
|
4
|
+
from exponent.utils.version import check_exponent_version, upgrade_exponent
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@exponent_cli_group()
|
|
8
|
+
def upgrade_cli() -> None:
|
|
9
|
+
"""Manage Indent version upgrades."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@upgrade_cli.command()
|
|
14
|
+
@click.option(
|
|
15
|
+
"--force",
|
|
16
|
+
is_flag=True,
|
|
17
|
+
help="Upgrade without prompting for confirmation, if a new version is available.",
|
|
18
|
+
)
|
|
19
|
+
def upgrade(force: bool = False) -> None:
|
|
20
|
+
"""Upgrade Indent to the latest version."""
|
|
21
|
+
if result := check_exponent_version():
|
|
22
|
+
installed_version, latest_version = result
|
|
23
|
+
upgrade_exponent(
|
|
24
|
+
current_version=installed_version,
|
|
25
|
+
new_version=latest_version,
|
|
26
|
+
force=force,
|
|
27
|
+
)
|
|
28
|
+
else:
|
|
29
|
+
click.echo("Indent is already up to date.")
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
import webbrowser
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from exponent.core.config import Environment, Settings
|
|
10
|
+
from exponent.utils.version import get_installed_version
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def print_editable_install_forced_prod_warning(settings: Settings) -> None:
|
|
14
|
+
click.secho(
|
|
15
|
+
"Detected local editable install, but this command only works against prod.",
|
|
16
|
+
fg="red",
|
|
17
|
+
bold=True,
|
|
18
|
+
)
|
|
19
|
+
click.secho("Using prod settings:", fg="red", bold=True)
|
|
20
|
+
click.secho("- base_url=", fg="yellow", bold=True, nl=False)
|
|
21
|
+
click.secho(f"{settings.base_url}", fg=(100, 200, 255), bold=False)
|
|
22
|
+
click.secho("- base_api_url=", fg="yellow", bold=True, nl=False)
|
|
23
|
+
click.secho(f"{settings.get_base_api_url()}", fg=(100, 200, 255), bold=False)
|
|
24
|
+
click.secho()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def print_editable_install_warning(settings: Settings) -> None:
|
|
28
|
+
click.secho(
|
|
29
|
+
"Detected local editable install, using local URLs", fg="yellow", bold=True
|
|
30
|
+
)
|
|
31
|
+
click.secho("- base_url=", fg="yellow", bold=True, nl=False)
|
|
32
|
+
click.secho(f"{settings.base_url}", fg=(100, 200, 255), bold=False)
|
|
33
|
+
click.secho("- base_api_url=", fg="yellow", bold=True, nl=False)
|
|
34
|
+
click.secho(f"{settings.get_base_api_url()}", fg=(100, 200, 255), bold=False)
|
|
35
|
+
click.secho()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def print_exponent_message(base_url: str, chat_uuid: str) -> None:
|
|
39
|
+
version = get_installed_version()
|
|
40
|
+
shell = os.environ.get("SHELL")
|
|
41
|
+
|
|
42
|
+
click.echo()
|
|
43
|
+
click.secho(f"△ Indent v{version}", fg=(180, 150, 255), bold=True)
|
|
44
|
+
click.echo()
|
|
45
|
+
click.echo(
|
|
46
|
+
" - Link: " + click.style(f"{base_url}/chats/{chat_uuid}", fg=(100, 200, 255))
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if shell is not None:
|
|
50
|
+
click.echo(f" - Shell: {shell}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_indent_app_installed() -> bool:
|
|
54
|
+
if sys.platform == "darwin": # macOS
|
|
55
|
+
return os.path.exists("/Applications/Indent.app")
|
|
56
|
+
|
|
57
|
+
# TODO: Add support for Windows and Linux
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def launch_exponent_browser(
|
|
62
|
+
environment: Environment, base_url: str, chat_uuid: str
|
|
63
|
+
) -> None:
|
|
64
|
+
if is_indent_app_installed() and environment == Environment.production:
|
|
65
|
+
url = f"exponent://chats/{chat_uuid}"
|
|
66
|
+
else:
|
|
67
|
+
url = f"{base_url}/chats/{chat_uuid}"
|
|
68
|
+
webbrowser.open(url)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Spinner:
|
|
72
|
+
def __init__(self, text: str) -> None:
|
|
73
|
+
self.text = text
|
|
74
|
+
self.task: asyncio.Task[None] | None = None
|
|
75
|
+
self.base_time = time.time()
|
|
76
|
+
self.fg_color: tuple[int, int, int] | None = None
|
|
77
|
+
self.bold = False
|
|
78
|
+
self.animation_chars = "⣷⣯⣟⡿⢿⣻⣽⣾"
|
|
79
|
+
self.animation_speed = 10
|
|
80
|
+
|
|
81
|
+
def show(self) -> None:
|
|
82
|
+
if self.task is not None:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
async def spinner(base_time: float) -> None:
|
|
86
|
+
color_start = ""
|
|
87
|
+
if self.fg_color:
|
|
88
|
+
if isinstance(self.fg_color, tuple) and len(self.fg_color) == 3:
|
|
89
|
+
r, g, b = self.fg_color
|
|
90
|
+
color_start = f"\x1b[38;2;{r};{g};{b}m"
|
|
91
|
+
elif isinstance(self.fg_color, int):
|
|
92
|
+
color_start = f"\x1b[{30 + self.fg_color}m"
|
|
93
|
+
|
|
94
|
+
bold_start = "\x1b[1m" if self.bold else ""
|
|
95
|
+
style_start = f"{color_start}{bold_start}"
|
|
96
|
+
style_end = "\x1b[0m"
|
|
97
|
+
|
|
98
|
+
while True:
|
|
99
|
+
t = time.time() - base_time
|
|
100
|
+
i = round(t * self.animation_speed) % len(self.animation_chars)
|
|
101
|
+
click.echo(
|
|
102
|
+
f"\r{style_start}{self.animation_chars[i]} {self.text}{style_end}",
|
|
103
|
+
nl=False,
|
|
104
|
+
)
|
|
105
|
+
await asyncio.sleep(0.1)
|
|
106
|
+
|
|
107
|
+
self.task = asyncio.get_event_loop().create_task(spinner(self.base_time))
|
|
108
|
+
|
|
109
|
+
def hide(self) -> None:
|
|
110
|
+
if self.task is None:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
self.task.cancel()
|
|
114
|
+
self.task = None
|
|
115
|
+
click.echo("\r\x1b[0m\x1b[2K", nl=False)
|
|
116
|
+
sys.stdout.flush()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ConnectionTracker:
|
|
120
|
+
def __init__(self) -> None:
|
|
121
|
+
self.connected = True
|
|
122
|
+
self.queue: asyncio.Queue[bool] = asyncio.Queue()
|
|
123
|
+
|
|
124
|
+
def is_connected(self) -> bool:
|
|
125
|
+
while True:
|
|
126
|
+
try:
|
|
127
|
+
self.connected = self.queue.get_nowait()
|
|
128
|
+
except asyncio.QueueEmpty:
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
return self.connected
|
|
132
|
+
|
|
133
|
+
async def wait_for_reconnection(self) -> None:
|
|
134
|
+
if not self.is_connected():
|
|
135
|
+
assert await self.queue.get()
|
|
136
|
+
self.connected = True
|
|
137
|
+
|
|
138
|
+
async def set_connected(self, connected: bool) -> None:
|
|
139
|
+
await self.queue.put(connected)
|
|
140
|
+
|
|
141
|
+
async def next_change(self) -> bool:
|
|
142
|
+
return await self.queue.get()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_short_git_commit_hash(commit_hash: str) -> str:
|
|
146
|
+
return commit_hash[:8]
|
exponent/core/config.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from importlib.metadata import Distribution, PackageNotFoundError
|
|
9
|
+
from typing import Any, Dict # noqa: UP035
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
from pydantic_settings import (
|
|
13
|
+
BaseSettings,
|
|
14
|
+
JsonConfigSettingsSource,
|
|
15
|
+
PydanticBaseSettingsSource,
|
|
16
|
+
SettingsConfigDict,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# If the package is editable, we want to use:
|
|
22
|
+
# base_url = localhost:3000
|
|
23
|
+
# base_api_url = localhost:8000
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_editable_install() -> bool:
|
|
27
|
+
if os.getenv("ENVIRONMENT") == "test" or os.getenv("EXPONENT_TEST_AUTO_UPGRADE"):
|
|
28
|
+
# We should explicitly set these variables
|
|
29
|
+
# in test when needed
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
dist = Distribution.from_name("indent")
|
|
34
|
+
except PackageNotFoundError:
|
|
35
|
+
logger.info("No distribution info found for indent")
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
direct_url = dist.read_text("direct_url.json")
|
|
39
|
+
if not direct_url:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
direct_url_json = json.loads(direct_url)
|
|
44
|
+
except json.JSONDecodeError:
|
|
45
|
+
logger.warning("Failed to decode distribution info for exponent-run")
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
pkg_is_editable = direct_url_json.get("dir_info", {}).get("editable", False)
|
|
49
|
+
return bool(pkg_is_editable)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Environment(str, enum.Enum):
|
|
53
|
+
test = "test"
|
|
54
|
+
development = "development"
|
|
55
|
+
staging = "staging"
|
|
56
|
+
production = "production"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_local(self) -> bool:
|
|
60
|
+
return self in [Environment.development, Environment.test]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class APIKeys(BaseModel):
|
|
64
|
+
development: str | None = None
|
|
65
|
+
staging: str | None = None
|
|
66
|
+
production: str | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
SETTINGS_FOLDER = os.path.expanduser("~/.config/indent")
|
|
70
|
+
SETTINGS_FILE_PATH = os.path.join(SETTINGS_FOLDER, "config.json")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class GlobalExponentOptions(BaseModel):
|
|
74
|
+
git_warning_disabled: bool = False
|
|
75
|
+
auto_upgrade: bool = True
|
|
76
|
+
base_api_url_override: str | None = None
|
|
77
|
+
base_ws_url_override: str | None = None
|
|
78
|
+
use_default_colors: bool = False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Settings(BaseSettings):
|
|
82
|
+
environment: Environment
|
|
83
|
+
base_url: str
|
|
84
|
+
base_api_url: str
|
|
85
|
+
base_ws_url: str
|
|
86
|
+
|
|
87
|
+
exponent_api_key: str | None = None
|
|
88
|
+
extra_exponent_api_keys: Dict[str, str] = {} # noqa: UP006
|
|
89
|
+
|
|
90
|
+
options: GlobalExponentOptions = GlobalExponentOptions()
|
|
91
|
+
|
|
92
|
+
log_level: str = "WARNING"
|
|
93
|
+
|
|
94
|
+
model_config = SettingsConfigDict(
|
|
95
|
+
json_file=SETTINGS_FILE_PATH,
|
|
96
|
+
json_file_encoding="utf-8",
|
|
97
|
+
env_prefix="EXPONENT_",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def get_base_api_url(self) -> str:
|
|
101
|
+
return self.options.base_api_url_override or self.base_api_url
|
|
102
|
+
|
|
103
|
+
def get_base_ws_url(self) -> str:
|
|
104
|
+
return self.options.base_ws_url_override or self.base_ws_url
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def settings_customise_sources(
|
|
108
|
+
cls,
|
|
109
|
+
settings_cls: type[BaseSettings],
|
|
110
|
+
init_settings: PydanticBaseSettingsSource,
|
|
111
|
+
env_settings: PydanticBaseSettingsSource,
|
|
112
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
|
113
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
|
114
|
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
115
|
+
return env_settings, JsonConfigSettingsSource(settings_cls), init_settings
|
|
116
|
+
|
|
117
|
+
def write_settings_to_config_file(self) -> None:
|
|
118
|
+
os.makedirs(SETTINGS_FOLDER, exist_ok=True)
|
|
119
|
+
with open(SETTINGS_FILE_PATH, "w") as f:
|
|
120
|
+
f.write(
|
|
121
|
+
self.model_dump_json(
|
|
122
|
+
include=self.config_file_keys, exclude_defaults=True
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def get_config_file_settings(self) -> dict[str, Any]:
|
|
127
|
+
return self.model_dump(include=self.config_file_keys, exclude_defaults=True)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def config_file_path(self) -> str:
|
|
131
|
+
return SETTINGS_FILE_PATH
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def config_file_keys(self) -> set[str]:
|
|
135
|
+
return {"exponent_api_key", "extra_exponent_api_keys", "options"}
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def api_key(self) -> str | None:
|
|
139
|
+
if self.environment.is_local:
|
|
140
|
+
return self.extra_exponent_api_keys.get(Environment.development)
|
|
141
|
+
elif self.environment == Environment.staging:
|
|
142
|
+
return self.extra_exponent_api_keys.get(Environment.staging)
|
|
143
|
+
elif self.environment == Environment.production:
|
|
144
|
+
return self.exponent_api_key
|
|
145
|
+
else:
|
|
146
|
+
raise ValueError(f"Unknown environment: {self.environment}")
|
|
147
|
+
|
|
148
|
+
def update_api_key(self, api_key: str) -> None:
|
|
149
|
+
if self.environment == Environment.development:
|
|
150
|
+
self.extra_exponent_api_keys[Environment.development] = api_key
|
|
151
|
+
elif self.environment == Environment.staging:
|
|
152
|
+
self.extra_exponent_api_keys[Environment.staging] = api_key
|
|
153
|
+
elif self.environment == Environment.production:
|
|
154
|
+
self.exponent_api_key = api_key
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@lru_cache(maxsize=1)
|
|
158
|
+
def get_settings(use_prod: bool = False, use_staging: bool = False) -> Settings:
|
|
159
|
+
if is_editable_install() and not (use_prod or use_staging):
|
|
160
|
+
base_url = "http://localhost:3000"
|
|
161
|
+
base_api_url = "http://localhost:8000"
|
|
162
|
+
base_ws_url = "ws://localhost:8000"
|
|
163
|
+
environment = Environment.development
|
|
164
|
+
elif use_staging:
|
|
165
|
+
base_url = "https://staging.indent.com"
|
|
166
|
+
base_api_url = "https://staging-api.indent.com"
|
|
167
|
+
base_ws_url = "wss://ws-staging-api.indent.com"
|
|
168
|
+
environment = Environment.staging
|
|
169
|
+
else:
|
|
170
|
+
base_url = "https://app.indent.com"
|
|
171
|
+
base_api_url = "https://api.indent.com"
|
|
172
|
+
base_ws_url = "wss://ws-api.indent.com"
|
|
173
|
+
environment = Environment.production
|
|
174
|
+
|
|
175
|
+
return Settings(
|
|
176
|
+
base_url=base_url,
|
|
177
|
+
base_api_url=base_api_url,
|
|
178
|
+
base_ws_url=base_ws_url,
|
|
179
|
+
environment=environment,
|
|
180
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from gql import Client, GraphQLRequest, gql
|
|
5
|
+
from gql.transport.httpx import HTTPXAsyncTransport
|
|
6
|
+
from gql.transport.websockets import WebsocketsTransport
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GraphQLClient:
|
|
10
|
+
def __init__(self, api_key: str, base_api_url: str, base_ws_url: str):
|
|
11
|
+
self.graphql_url = f"{base_api_url}/graphql"
|
|
12
|
+
self.websocket_url = f"{base_ws_url}/graphql_ws".replace(
|
|
13
|
+
"https", "wss"
|
|
14
|
+
).replace("http", "ws")
|
|
15
|
+
|
|
16
|
+
self.api_key = api_key
|
|
17
|
+
|
|
18
|
+
def get_transport(self, timeout: float | None = None) -> HTTPXAsyncTransport:
|
|
19
|
+
return HTTPXAsyncTransport(
|
|
20
|
+
url=self.graphql_url,
|
|
21
|
+
headers={"API-KEY": self.api_key},
|
|
22
|
+
timeout=timeout,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def get_ws_transport(self) -> WebsocketsTransport:
|
|
26
|
+
return WebsocketsTransport(
|
|
27
|
+
url=self.websocket_url,
|
|
28
|
+
init_payload={"apiKey": self.api_key},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
async def execute(
|
|
32
|
+
self,
|
|
33
|
+
query_str: str,
|
|
34
|
+
vars: dict[str, Any] | None = None,
|
|
35
|
+
op_name: str | None = None,
|
|
36
|
+
timeout: float | None = None,
|
|
37
|
+
) -> Any:
|
|
38
|
+
async with Client(
|
|
39
|
+
transport=self.get_transport(timeout),
|
|
40
|
+
fetch_schema_from_transport=False,
|
|
41
|
+
execute_timeout=timeout,
|
|
42
|
+
) as session:
|
|
43
|
+
# Execute single query
|
|
44
|
+
query = GraphQLRequest(
|
|
45
|
+
query_str, variable_values=vars, operation_name=op_name
|
|
46
|
+
)
|
|
47
|
+
result = await session.execute(query)
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
async def subscribe(
|
|
51
|
+
self,
|
|
52
|
+
subscription_str: str,
|
|
53
|
+
vars: dict[str, Any] | None = None,
|
|
54
|
+
) -> AsyncGenerator[dict[str, Any], None]:
|
|
55
|
+
async with Client(
|
|
56
|
+
transport=self.get_ws_transport(),
|
|
57
|
+
) as session:
|
|
58
|
+
# Execute subscription
|
|
59
|
+
subscription = gql(subscription_str)
|
|
60
|
+
async for result in session.subscribe(subscription, vars):
|
|
61
|
+
yield result
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
GET_CHATS_QUERY: str = """
|
|
2
|
+
query Chats {
|
|
3
|
+
chats {
|
|
4
|
+
... on UnauthenticatedError {
|
|
5
|
+
message
|
|
6
|
+
}
|
|
7
|
+
... on Chats {
|
|
8
|
+
chats {
|
|
9
|
+
id
|
|
10
|
+
chatUuid
|
|
11
|
+
name
|
|
12
|
+
subtitle
|
|
13
|
+
isShared
|
|
14
|
+
isStarted
|
|
15
|
+
updatedAt # ISO datetime string
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
"""
|
|
21
|
+
CREATE_CLOUD_CONFIG_MUTATION: str = """
|
|
22
|
+
mutation CreateCloudConfig(
|
|
23
|
+
$githubOrgName: String!,
|
|
24
|
+
$githubRepoName: String!,
|
|
25
|
+
$setupCommands: [String!],
|
|
26
|
+
) {
|
|
27
|
+
createCloudConfig(
|
|
28
|
+
input: {
|
|
29
|
+
githubOrgName: $githubOrgName,
|
|
30
|
+
githubRepoName: $githubRepoName,
|
|
31
|
+
setupCommands: $setupCommands,
|
|
32
|
+
}
|
|
33
|
+
) {
|
|
34
|
+
__typename
|
|
35
|
+
... on UnauthenticatedError {
|
|
36
|
+
message
|
|
37
|
+
}
|
|
38
|
+
... on CloudConfig {
|
|
39
|
+
cloudConfigUuid
|
|
40
|
+
githubOrgName
|
|
41
|
+
githubRepoName
|
|
42
|
+
setupCommands
|
|
43
|
+
repoUrl
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
"""
|