openhands-agent-server 1.8.2__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.
- openhands/agent_server/__init__.py +0 -0
- openhands/agent_server/__main__.py +118 -0
- openhands/agent_server/api.py +331 -0
- openhands/agent_server/bash_router.py +105 -0
- openhands/agent_server/bash_service.py +379 -0
- openhands/agent_server/config.py +187 -0
- openhands/agent_server/conversation_router.py +321 -0
- openhands/agent_server/conversation_service.py +692 -0
- openhands/agent_server/dependencies.py +72 -0
- openhands/agent_server/desktop_router.py +47 -0
- openhands/agent_server/desktop_service.py +212 -0
- openhands/agent_server/docker/Dockerfile +244 -0
- openhands/agent_server/docker/build.py +825 -0
- openhands/agent_server/docker/wallpaper.svg +22 -0
- openhands/agent_server/env_parser.py +460 -0
- openhands/agent_server/event_router.py +204 -0
- openhands/agent_server/event_service.py +648 -0
- openhands/agent_server/file_router.py +121 -0
- openhands/agent_server/git_router.py +34 -0
- openhands/agent_server/logging_config.py +56 -0
- openhands/agent_server/middleware.py +32 -0
- openhands/agent_server/models.py +307 -0
- openhands/agent_server/openapi.py +21 -0
- openhands/agent_server/pub_sub.py +80 -0
- openhands/agent_server/py.typed +0 -0
- openhands/agent_server/server_details_router.py +43 -0
- openhands/agent_server/sockets.py +173 -0
- openhands/agent_server/tool_preload_service.py +76 -0
- openhands/agent_server/tool_router.py +22 -0
- openhands/agent_server/utils.py +63 -0
- openhands/agent_server/vscode_extensions/openhands-settings/extension.js +22 -0
- openhands/agent_server/vscode_extensions/openhands-settings/package.json +12 -0
- openhands/agent_server/vscode_router.py +70 -0
- openhands/agent_server/vscode_service.py +232 -0
- openhands_agent_server-1.8.2.dist-info/METADATA +15 -0
- openhands_agent_server-1.8.2.dist-info/RECORD +39 -0
- openhands_agent_server-1.8.2.dist-info/WHEEL +5 -0
- openhands_agent_server-1.8.2.dist-info/entry_points.txt +2 -0
- openhands_agent_server-1.8.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<svg width="774" height="460" viewBox="0 0 774 460" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="774" height="460" fill="#F1EAE0"/>
|
|
3
|
+
<g clip-path="url(#clip0_4872_6467)">
|
|
4
|
+
<g clip-path="url(#clip1_4872_6467)">
|
|
5
|
+
<path d="M475.531 192.089C468.665 187.872 464.077 194.334 464.64 203.108L464.583 203.175C464.602 194.012 463.362 183.892 459.242 175.706C457.783 172.806 454.827 168.04 448.98 170.286C446.415 171.271 444.088 174.237 445.29 181.893C445.29 181.893 446.625 189.928 446.377 200.029V200.171C444.708 172.314 438.413 163.814 429.411 164.364C426.531 164.876 422.592 166.117 423.917 174.682C423.917 174.682 425.357 183.618 425.825 190.734L425.853 191.094H425.825C421.59 175.734 415.887 175.526 411.758 176.123C408.01 176.663 403.919 180.548 405.988 188.204C412.483 212.224 411.214 241.152 410.728 245.293C409.402 242.459 408.992 240.214 407.152 237.106C399.751 224.627 396.232 223.708 391.912 223.518C387.62 223.329 382.985 225.972 383.29 231.004C383.605 236.035 386.17 236.869 389.813 243.881C392.655 249.338 393.466 256.492 399.188 269.492C403.928 280.256 416.316 292.063 438.881 290.66C457.163 290.054 484.467 283.668 479.718 241.73C478.535 234.443 479.422 228.341 480.042 222.087C481.005 212.385 482.417 196.305 475.541 192.079L475.531 192.089Z" fill="#FFE165"/>
|
|
6
|
+
<path d="M361.374 223.812C357.054 224.087 353.564 225.063 346.392 237.674C344.609 240.811 344.246 243.066 342.968 245.918C342.406 241.787 340.603 212.887 346.649 188.753C348.576 181.069 344.418 177.26 340.66 176.786C336.521 176.265 330.809 176.578 326.86 192.089H326.813L326.87 191.643C327.204 184.518 328.482 175.564 328.482 175.564C329.636 166.97 325.687 165.804 322.798 165.34C313.814 164.961 307.691 173.498 306.509 201.109H306.49C306.079 191.113 307.252 183.153 307.252 183.153C308.311 175.469 305.927 172.551 303.342 171.613C297.458 169.471 294.587 174.294 293.186 177.222C289.218 185.484 288.16 195.623 288.35 204.786L288.293 204.719C288.684 195.936 283.982 189.559 277.192 193.898C270.402 198.248 272.109 214.299 273.253 223.983C273.997 230.227 274.989 236.31 273.94 243.615C269.973 285.629 297.391 291.513 315.683 291.788C338.266 292.783 350.436 280.74 354.975 269.89C360.449 256.796 361.136 249.623 363.873 244.118C367.383 237.04 369.939 236.158 370.158 231.127C370.377 226.096 365.695 223.537 361.403 223.803L361.374 223.812Z" fill="#FFE165"/>
|
|
7
|
+
<path d="M370.415 223.878C368.127 221.652 364.684 220.467 361.184 220.676C355.576 221.026 351.599 223.063 345.486 233.012C345.324 221.633 345.963 204.549 349.72 189.521C351.132 183.873 349.711 180.188 348.261 178.094C346.573 175.649 343.95 174.038 341.051 173.678C338.41 173.346 334.957 173.327 331.591 176.075C331.591 176.037 331.6 175.99 331.6 175.99C332.687 167.917 329.884 163.293 323.274 162.251L322.912 162.213C318.84 162.033 315.349 163.359 312.536 166.145C311.029 167.633 309.723 169.556 308.588 171.935C307.319 170.087 305.688 169.149 304.401 168.675C296.533 165.804 292.184 171.954 290.296 175.876C288.102 180.443 286.739 185.56 285.976 190.781C285.814 190.677 285.661 190.573 285.499 190.478C283.763 189.483 280.149 188.27 275.456 191.274C267.503 196.371 268.514 211.02 270.097 224.361C270.183 225.072 270.268 225.773 270.354 226.484C271.031 231.951 271.67 237.115 270.793 243.189L270.774 243.341C269.181 260.197 272.376 273.169 280.273 281.915C287.864 290.329 299.756 294.706 315.521 294.943C316.675 294.99 317.8 295.009 318.897 295C345.829 294.754 355.49 276.798 357.865 271.113C360.936 263.76 362.528 258.274 363.797 253.858C364.779 250.447 365.561 247.756 366.667 245.52C367.926 242.98 369.033 241.332 370.005 239.873C371.665 237.39 373.095 235.249 373.267 231.269C373.391 228.389 372.399 225.83 370.387 223.878H370.415ZM317.009 170.599C318.525 169.101 320.27 168.438 322.483 168.486C324.4 168.798 326.136 169.263 325.344 175.137C325.287 175.507 324.047 184.328 323.713 191.511C323.713 191.558 323.713 191.605 323.713 191.653C321.968 198.664 320.509 208.765 319.727 223.528C316.341 223.736 312.965 224.096 309.684 224.57C308.626 194.618 311.077 176.454 316.999 170.599H317.009ZM296.018 178.586C298.393 173.659 300.386 173.905 302.236 174.578C304.801 175.516 304.401 180.605 304.105 182.717C304.058 183.059 302.913 190.989 303.314 201.081C303.018 208.159 303.056 216.317 303.409 225.65C300.271 226.266 297.296 226.977 294.578 227.744C293.29 223.442 287.597 196.116 296.018 178.596V178.586ZM364.76 236.395C363.73 237.93 362.452 239.844 361.012 242.744C359.648 245.482 358.809 248.419 357.731 252.124C356.501 256.379 354.966 261.675 352.038 268.696C349.959 273.662 341.223 289.789 315.712 288.652C301.492 288.443 291.44 284.861 284.984 277.708C278.327 270.336 275.666 258.984 277.068 243.985C278.041 237.125 277.326 231.316 276.629 225.707C276.544 225.006 276.458 224.314 276.372 223.613C275.609 217.141 273.568 199.944 278.88 196.542C280.311 195.623 281.484 195.414 282.352 195.907C283.83 196.75 285.318 199.82 285.108 204.577C285.098 204.833 285.127 205.079 285.175 205.326C285.442 216.412 287.435 226.058 288.57 229.677C286.71 230.369 285.07 231.089 283.696 231.809C282.151 232.624 281.57 234.519 282.39 236.054C282.962 237.125 284.068 237.731 285.213 237.722C285.699 237.722 286.195 237.599 286.662 237.352C294.378 233.287 312.679 229.156 328.806 229.601C330.561 229.63 332.001 228.284 332.048 226.55C332.096 224.816 330.723 223.376 328.978 223.329C328.014 223.3 327.042 223.3 326.069 223.3C327.681 193.529 332.115 183.817 335.568 180.946C336.998 179.761 338.362 179.666 340.25 179.903C340.775 179.97 342.091 180.273 343.035 181.637C344.055 183.125 344.246 185.323 343.579 187.995C337.742 211.314 339.058 238.736 339.726 245.387C339.611 245.615 339.506 245.842 339.382 246.079C338.019 248.628 335.482 251.281 332.459 251.101C330.732 251.016 329.216 252.323 329.111 254.048C329.006 255.782 330.332 257.269 332.077 257.373C337.189 257.677 342.005 254.559 344.961 249.035C345.276 248.448 345.543 247.889 345.782 247.349C345.801 247.311 345.82 247.263 345.839 247.225C346.392 245.994 346.792 244.866 347.145 243.852C347.698 242.279 348.175 240.915 349.129 239.228C355.9 227.308 358.647 227.138 361.556 226.958C363.263 226.854 364.96 227.394 365.962 228.379C366.677 229.071 366.992 229.933 366.944 231.013C366.849 233.24 366.229 234.168 364.731 236.405L364.76 236.395Z" fill="black"/>
|
|
8
|
+
<path d="M482.817 241.237C481.835 235.182 482.379 230.009 482.951 224.532C483.027 223.821 483.104 223.12 483.17 222.41C484.505 209.049 485.249 194.372 477.191 189.426C472.441 186.508 468.846 187.787 467.129 188.81C466.967 188.905 466.814 189.019 466.652 189.123C465.784 183.921 464.335 178.833 462.056 174.304C460.1 170.419 455.647 164.345 447.826 167.358C446.549 167.851 444.946 168.817 443.707 170.693C442.524 168.334 441.179 166.439 439.644 164.98C436.783 162.251 433.264 160.981 429.201 161.237L428.839 161.275C422.249 162.44 419.53 167.112 420.77 175.185C420.77 175.185 420.77 175.223 420.78 175.251C417.366 172.56 413.913 172.645 411.281 173.024C408.391 173.441 405.797 175.099 404.157 177.572C402.755 179.695 401.391 183.4 402.908 189.019C406.951 203.98 407.905 221.055 407.953 232.435C401.658 222.599 397.643 220.638 392.036 220.391C388.545 220.24 385.102 221.5 382.861 223.765C380.887 225.754 379.943 228.332 380.124 231.203C380.372 235.173 381.841 237.295 383.548 239.74C384.549 241.18 385.684 242.81 386.991 245.321C388.145 247.538 388.974 250.21 390.023 253.602C391.378 257.989 393.066 263.447 396.28 270.743C398.759 276.381 408.754 294.166 435.61 293.91C436.697 293.901 437.822 293.863 438.967 293.787C454.817 293.266 466.624 288.661 474.063 280.114C481.787 271.226 484.744 258.198 482.846 241.37L482.827 241.218L482.817 241.237ZM448.399 181.419C448.065 179.278 447.559 174.199 450.115 173.223C451.946 172.513 453.949 172.238 456.41 177.118C465.155 194.486 459.976 221.917 458.765 226.238C456.028 225.518 453.043 224.864 449.896 224.305C450.068 214.972 449.953 206.804 449.534 199.735C449.743 189.644 448.456 181.732 448.399 181.419ZM429.754 167.5C431.976 167.405 433.731 168.04 435.266 169.518C441.294 175.27 444.088 193.377 443.583 223.348C440.292 222.931 436.916 222.637 433.521 222.485C432.472 207.732 430.822 197.67 428.943 190.686C428.943 190.639 428.943 190.592 428.943 190.544C428.476 183.362 427.065 174.569 427.007 174.228C426.101 168.344 427.828 167.851 429.745 167.5H429.754ZM476.571 242.166C478.249 257.137 475.798 268.535 469.285 276.03C462.962 283.298 452.976 287.069 438.671 287.543C413.303 289.135 404.243 273.178 402.078 268.251C399.007 261.277 397.376 256.019 396.07 251.783C394.925 248.088 394.029 245.179 392.617 242.459C391.13 239.588 389.813 237.703 388.755 236.187C387.219 233.979 386.58 233.06 386.437 230.833C386.371 229.753 386.676 228.881 387.372 228.18C388.364 227.176 390.042 226.598 391.759 226.683C394.668 226.816 397.414 226.929 404.414 238.726C405.406 240.394 405.902 241.749 406.484 243.312C406.865 244.326 407.285 245.454 407.867 246.676C407.886 246.714 407.895 246.752 407.915 246.78C408.172 247.32 408.449 247.87 408.773 248.457C411.834 253.925 416.708 256.957 421.81 256.559C423.546 256.426 424.852 254.91 424.719 253.185C424.585 251.461 423.069 250.182 421.323 250.296C418.3 250.523 415.716 247.917 414.304 245.397C414.171 245.16 414.066 244.942 413.951 244.715C414.495 238.063 415.306 210.613 409.03 187.408C408.306 184.745 408.458 182.547 409.45 181.04C410.375 179.657 411.682 179.325 412.206 179.25C414.085 178.975 415.458 179.051 416.908 180.207C420.417 183.021 425.033 192.648 427.189 222.381C426.216 222.391 425.243 222.419 424.289 222.467C422.544 222.542 421.199 224.011 421.276 225.745C421.352 227.479 422.802 228.787 424.576 228.739C440.683 228 459.07 231.79 466.853 235.722C467.32 235.959 467.816 236.064 468.312 236.064C469.456 236.054 470.553 235.429 471.106 234.339C471.898 232.795 471.278 230.9 469.714 230.113C468.331 229.412 466.671 228.73 464.802 228.066C465.87 224.428 467.692 214.744 467.749 203.658C467.797 203.412 467.816 203.165 467.797 202.909C467.492 198.162 468.932 195.064 470.391 194.192C471.249 193.681 472.422 193.87 473.872 194.761C479.251 198.068 477.534 215.294 476.885 221.784C476.819 222.485 476.742 223.177 476.666 223.878C476.075 229.507 475.464 235.315 476.571 242.166Z" fill="black"/>
|
|
9
|
+
<path d="M388.145 181.581C387.639 181.581 387.124 181.486 386.638 181.258C384.854 180.443 384.072 178.349 384.892 176.578C387.544 170.826 391.425 165.52 396.108 161.237C397.548 159.92 399.799 160.005 401.124 161.445C402.45 162.876 402.364 165.112 400.914 166.429C396.918 170.087 393.609 174.616 391.339 179.524C390.748 180.804 389.48 181.571 388.145 181.581Z" fill="black"/>
|
|
10
|
+
<path d="M376.185 179.543C374.345 179.562 372.771 178.16 372.618 176.303C371.941 167.993 371.913 159.569 372.552 151.26C372.704 149.317 374.411 147.867 376.357 148.009C378.312 148.161 379.771 149.848 379.628 151.79C379.018 159.73 379.046 167.784 379.695 175.725C379.857 177.667 378.398 179.373 376.443 179.524C376.357 179.524 376.271 179.534 376.185 179.534V179.543Z" fill="black"/>
|
|
11
|
+
<path d="M363.959 181.732C362.366 181.751 360.898 180.68 360.488 179.079C358.981 173.204 356.129 167.623 352.248 162.952C350.998 161.445 351.218 159.228 352.724 157.987C354.241 156.746 356.472 156.964 357.722 158.461C362.261 163.938 365.599 170.466 367.354 177.336C367.84 179.221 366.686 181.145 364.789 181.628C364.503 181.704 364.226 181.732 363.94 181.742L363.959 181.732Z" fill="black"/>
|
|
12
|
+
</g>
|
|
13
|
+
</g>
|
|
14
|
+
<defs>
|
|
15
|
+
<clipPath id="clip0_4872_6467">
|
|
16
|
+
<rect width="215" height="147" fill="white" transform="translate(269 148)"/>
|
|
17
|
+
</clipPath>
|
|
18
|
+
<clipPath id="clip1_4872_6467">
|
|
19
|
+
<rect width="215" height="147" fill="white" transform="translate(269 148)"/>
|
|
20
|
+
</clipPath>
|
|
21
|
+
</defs>
|
|
22
|
+
</svg>
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""Utility for converting environment variables into pydantic base models.
|
|
2
|
+
We couldn't use pydantic-settings for this as we need complex nested types
|
|
3
|
+
and polymorphism."""
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from io import StringIO
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from types import UnionType
|
|
15
|
+
from typing import IO, Annotated, Any, Literal, Union, cast, get_args, get_origin
|
|
16
|
+
from uuid import UUID
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, SecretStr, TypeAdapter
|
|
19
|
+
|
|
20
|
+
from openhands.sdk.utils.models import (
|
|
21
|
+
DiscriminatedUnionMixin,
|
|
22
|
+
get_known_concrete_subclasses,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Define Missing type
|
|
27
|
+
class MissingType:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
MISSING = MissingType()
|
|
32
|
+
JsonType = str | int | float | bool | dict | list | None | MissingType
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EnvParser(ABC):
|
|
36
|
+
"""Event parser type"""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def from_env(self, key: str) -> JsonType:
|
|
40
|
+
"""Parse environment variables into a json like structure"""
|
|
41
|
+
|
|
42
|
+
def to_env(self, key: str, value: Any, output: IO):
|
|
43
|
+
"""Produce a template based on this parser"""
|
|
44
|
+
if value is None:
|
|
45
|
+
value = ""
|
|
46
|
+
output.write(f"{key}={value}\n")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class BoolEnvParser(EnvParser):
|
|
50
|
+
def from_env(self, key: str) -> bool | MissingType:
|
|
51
|
+
if key not in os.environ:
|
|
52
|
+
return MISSING
|
|
53
|
+
return os.environ[key].upper() in ["1", "TRUE"] # type: ignore
|
|
54
|
+
|
|
55
|
+
def to_env(self, key: str, value: Any, output: IO):
|
|
56
|
+
output.write(f"{key}={1 if value else 0}\n")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class IntEnvParser(EnvParser):
|
|
60
|
+
def from_env(self, key: str) -> int | MissingType:
|
|
61
|
+
if key not in os.environ:
|
|
62
|
+
return MISSING
|
|
63
|
+
return int(os.environ[key])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FloatEnvParser(EnvParser):
|
|
67
|
+
def from_env(self, key: str) -> float | MissingType:
|
|
68
|
+
if key not in os.environ:
|
|
69
|
+
return MISSING
|
|
70
|
+
return float(os.environ[key])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class StrEnvParser(EnvParser):
|
|
74
|
+
def from_env(self, key: str) -> str | MissingType:
|
|
75
|
+
if key not in os.environ:
|
|
76
|
+
return MISSING
|
|
77
|
+
return os.environ[key]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class NoneEnvParser(EnvParser):
|
|
81
|
+
def from_env(self, key: str) -> None | MissingType:
|
|
82
|
+
key = f"{key}_IS_NONE"
|
|
83
|
+
value = (os.getenv(key) or "").upper()
|
|
84
|
+
if value in ["1", "TRUE"]:
|
|
85
|
+
return None
|
|
86
|
+
return MISSING
|
|
87
|
+
|
|
88
|
+
def to_env(self, key: str, value: Any, output: IO):
|
|
89
|
+
if value is None:
|
|
90
|
+
output.write(f"{key}_IS_NONE=1\n")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class LiteralEnvParser(EnvParser):
|
|
95
|
+
values: tuple[str, ...]
|
|
96
|
+
|
|
97
|
+
def from_env(self, key: str) -> str | MissingType:
|
|
98
|
+
value = os.getenv(key)
|
|
99
|
+
if value not in self.values:
|
|
100
|
+
return MISSING
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
def to_env(self, key: str, value: Any, output: IO):
|
|
104
|
+
output.write(f"# Permitted Values: {', '.join(self.values)}\n")
|
|
105
|
+
# For enums, use the value instead of the string representation
|
|
106
|
+
if hasattr(value, "value"):
|
|
107
|
+
output.write(f"{key}={value.value}\n")
|
|
108
|
+
else:
|
|
109
|
+
output.write(f"{key}={value}\n")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class ModelEnvParser(EnvParser):
|
|
114
|
+
parsers: dict[str, EnvParser]
|
|
115
|
+
descriptions: dict[str, str]
|
|
116
|
+
|
|
117
|
+
def from_env(self, key: str) -> dict | MissingType:
|
|
118
|
+
# First we see is there a base value defined as json...
|
|
119
|
+
value = os.environ.get(key)
|
|
120
|
+
if value:
|
|
121
|
+
result = json.loads(value)
|
|
122
|
+
assert isinstance(result, dict)
|
|
123
|
+
else:
|
|
124
|
+
result = MISSING
|
|
125
|
+
|
|
126
|
+
# Check for overrides...
|
|
127
|
+
for field_name, parser in self.parsers.items():
|
|
128
|
+
env_var_name = f"{key}_{field_name.upper()}"
|
|
129
|
+
|
|
130
|
+
# First we check that there are possible keys for this field to prevent
|
|
131
|
+
# infinite recursion
|
|
132
|
+
has_possible_keys = next(
|
|
133
|
+
(k for k in os.environ if k.startswith(env_var_name)), False
|
|
134
|
+
)
|
|
135
|
+
if not has_possible_keys:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
field_value = parser.from_env(env_var_name)
|
|
139
|
+
if field_value is MISSING:
|
|
140
|
+
continue
|
|
141
|
+
if result is MISSING:
|
|
142
|
+
result = {}
|
|
143
|
+
existing_field_value = result.get(field_name, MISSING) # type: ignore
|
|
144
|
+
new_field_value = merge(existing_field_value, field_value)
|
|
145
|
+
if new_field_value is not MISSING:
|
|
146
|
+
result[field_name] = new_field_value # type: ignore
|
|
147
|
+
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
def to_env(self, key: str, value: Any, output: IO):
|
|
151
|
+
for field_name, parser in self.parsers.items():
|
|
152
|
+
field_description = self.descriptions.get(field_name)
|
|
153
|
+
if field_description:
|
|
154
|
+
for line in field_description.split("\n"):
|
|
155
|
+
output.write("# ")
|
|
156
|
+
output.write(line)
|
|
157
|
+
output.write("\n")
|
|
158
|
+
field_key = key + "_" + field_name.upper()
|
|
159
|
+
field_value = getattr(value, field_name)
|
|
160
|
+
parser.to_env(field_key, field_value, output)
|
|
161
|
+
output.write("\n")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class DictEnvParser(EnvParser):
|
|
165
|
+
def from_env(self, key: str) -> dict | MissingType:
|
|
166
|
+
# Read json from an environment variable
|
|
167
|
+
value = os.environ.get(key)
|
|
168
|
+
if value:
|
|
169
|
+
result = json.loads(value)
|
|
170
|
+
assert isinstance(result, dict)
|
|
171
|
+
else:
|
|
172
|
+
result = MISSING
|
|
173
|
+
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class ListEnvParser(EnvParser):
|
|
179
|
+
item_parser: EnvParser
|
|
180
|
+
item_type: type
|
|
181
|
+
|
|
182
|
+
def from_env(self, key: str) -> list | MissingType:
|
|
183
|
+
if key not in os.environ:
|
|
184
|
+
# Try to read sequentially, starting with 0
|
|
185
|
+
# Return MISSING if there are no items
|
|
186
|
+
result = MISSING
|
|
187
|
+
index = 0
|
|
188
|
+
while True:
|
|
189
|
+
sub_key = f"{key}_{index}"
|
|
190
|
+
item = self.item_parser.from_env(sub_key)
|
|
191
|
+
if item is MISSING:
|
|
192
|
+
return result
|
|
193
|
+
if result is MISSING:
|
|
194
|
+
result = []
|
|
195
|
+
result.append(item) # type: ignore
|
|
196
|
+
index += 1
|
|
197
|
+
|
|
198
|
+
# Assume the value is json
|
|
199
|
+
value = os.environ.get(key)
|
|
200
|
+
result = json.loads(value) # type: ignore
|
|
201
|
+
# A number indicates that the result should be N items long
|
|
202
|
+
if isinstance(result, int):
|
|
203
|
+
result = [MISSING] * result
|
|
204
|
+
else:
|
|
205
|
+
# Otherwise assume the item is a list
|
|
206
|
+
assert isinstance(result, list)
|
|
207
|
+
|
|
208
|
+
for index in range(len(result)):
|
|
209
|
+
sub_key = f"{key}_{index}"
|
|
210
|
+
item = self.item_parser.from_env(sub_key)
|
|
211
|
+
item = merge(result[index], item)
|
|
212
|
+
# We permit missing items in the list because these may be filled
|
|
213
|
+
# in later when merged with the output of another parser
|
|
214
|
+
result[index] = item # type: ignore
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
def to_env(self, key: str, value: Any, output: IO):
|
|
219
|
+
if len(value):
|
|
220
|
+
for index, sub_value in enumerate(value):
|
|
221
|
+
sub_key = f"{key}_{index}"
|
|
222
|
+
self.item_parser.to_env(sub_key, sub_value, output)
|
|
223
|
+
else:
|
|
224
|
+
# Try to produce a sample value based on the defaults...
|
|
225
|
+
try:
|
|
226
|
+
sub_key = f"{key}_0"
|
|
227
|
+
sample_output = StringIO()
|
|
228
|
+
self.item_parser.to_env(
|
|
229
|
+
sub_key, _create_sample(self.item_type), sample_output
|
|
230
|
+
)
|
|
231
|
+
for line in sample_output.getvalue().strip().split("\n"):
|
|
232
|
+
output.write("# ")
|
|
233
|
+
output.write(line)
|
|
234
|
+
output.write("\n")
|
|
235
|
+
except Exception:
|
|
236
|
+
# Couldn't create a sample value. Skip
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass
|
|
241
|
+
class UnionEnvParser(EnvParser):
|
|
242
|
+
parsers: dict[type, EnvParser]
|
|
243
|
+
|
|
244
|
+
def from_env(self, key: str) -> JsonType:
|
|
245
|
+
result = MISSING
|
|
246
|
+
for parser in self.parsers.values():
|
|
247
|
+
parser_result = parser.from_env(key)
|
|
248
|
+
result = merge(result, parser_result)
|
|
249
|
+
return result
|
|
250
|
+
|
|
251
|
+
def to_env(self, key: str, value: Any, output: IO):
|
|
252
|
+
for type_, parser in self.parsers.items():
|
|
253
|
+
if not isinstance(value, type_):
|
|
254
|
+
# Try to produce a sample value based on the defaults...
|
|
255
|
+
try:
|
|
256
|
+
sample_value = _create_sample(type_)
|
|
257
|
+
sample_output = StringIO()
|
|
258
|
+
sample_output.write(f"{sample_value.__class__.__name__}\n")
|
|
259
|
+
parser.to_env(key, sample_value, sample_output)
|
|
260
|
+
for line in sample_output.getvalue().split("\n"):
|
|
261
|
+
output.write("# ")
|
|
262
|
+
output.write(line)
|
|
263
|
+
output.write("\n")
|
|
264
|
+
except Exception:
|
|
265
|
+
# Couldn't create a sample value. Skip
|
|
266
|
+
pass
|
|
267
|
+
for type_, parser in self.parsers.items():
|
|
268
|
+
if isinstance(value, type_):
|
|
269
|
+
output.write(f"# {value.__class__.__name__}\n")
|
|
270
|
+
parser.to_env(key, value, output)
|
|
271
|
+
output.write("\n")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@dataclass
|
|
275
|
+
class DiscriminatedUnionEnvParser(EnvParser):
|
|
276
|
+
parsers: dict[str, EnvParser]
|
|
277
|
+
|
|
278
|
+
def from_env(self, key: str) -> JsonType:
|
|
279
|
+
kind = os.environ.get(f"{key}_KIND", MISSING)
|
|
280
|
+
if kind is MISSING:
|
|
281
|
+
return MISSING
|
|
282
|
+
assert isinstance(kind, str)
|
|
283
|
+
parser = self.parsers[kind]
|
|
284
|
+
parser_result = parser.from_env(key)
|
|
285
|
+
assert isinstance(parser_result, dict)
|
|
286
|
+
parser_result["kind"] = kind
|
|
287
|
+
return parser_result
|
|
288
|
+
|
|
289
|
+
def to_env(self, key: str, value: Any, output: IO):
|
|
290
|
+
parser = self.parsers[value.kind]
|
|
291
|
+
parser.to_env(key, value, output)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass
|
|
295
|
+
class DelayedParser(EnvParser):
|
|
296
|
+
"""Delayed parser for circular dependencies"""
|
|
297
|
+
|
|
298
|
+
parser: EnvParser | None = None
|
|
299
|
+
|
|
300
|
+
def from_env(self, key: str) -> JsonType:
|
|
301
|
+
assert self.parser is not None
|
|
302
|
+
return self.parser.from_env(key)
|
|
303
|
+
|
|
304
|
+
def to_env(self, key: str, value: Any, output: IO):
|
|
305
|
+
assert self.parser is not None
|
|
306
|
+
return self.parser.to_env(key, value, output)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def merge(a, b):
|
|
310
|
+
if a is MISSING:
|
|
311
|
+
return b
|
|
312
|
+
if b is MISSING:
|
|
313
|
+
return a
|
|
314
|
+
if isinstance(a, dict) and isinstance(b, dict):
|
|
315
|
+
result = {**a}
|
|
316
|
+
for key, value in b.items():
|
|
317
|
+
result[key] = merge(result.get(key), value)
|
|
318
|
+
return result
|
|
319
|
+
if isinstance(a, list) and isinstance(b, list):
|
|
320
|
+
result = a.copy()
|
|
321
|
+
for index, value in enumerate(b):
|
|
322
|
+
if index >= len(a):
|
|
323
|
+
result[index] = value
|
|
324
|
+
else:
|
|
325
|
+
result[index] = merge(result[index], value)
|
|
326
|
+
return result
|
|
327
|
+
# Favor present values over missing ones
|
|
328
|
+
if b is None:
|
|
329
|
+
return a
|
|
330
|
+
# Later values overwrite earier ones
|
|
331
|
+
return b
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_env_parser(target_type: type, parsers: dict[type, EnvParser]) -> EnvParser:
|
|
335
|
+
# Check if we have already defined a parser
|
|
336
|
+
if target_type in parsers:
|
|
337
|
+
return parsers[target_type]
|
|
338
|
+
|
|
339
|
+
# Check origin
|
|
340
|
+
origin = get_origin(target_type)
|
|
341
|
+
if origin is Annotated:
|
|
342
|
+
# Strip annotations...
|
|
343
|
+
return get_env_parser(get_args(target_type)[0], parsers)
|
|
344
|
+
if origin is UnionType or origin is Union:
|
|
345
|
+
union_parsers = {
|
|
346
|
+
t: get_env_parser(t, parsers) # type: ignore
|
|
347
|
+
for t in get_args(target_type)
|
|
348
|
+
}
|
|
349
|
+
return UnionEnvParser(union_parsers)
|
|
350
|
+
if origin is list:
|
|
351
|
+
item_type = get_args(target_type)[0]
|
|
352
|
+
parser = get_env_parser(item_type, parsers)
|
|
353
|
+
return ListEnvParser(parser, item_type)
|
|
354
|
+
if origin is dict:
|
|
355
|
+
args = get_args(target_type)
|
|
356
|
+
assert args[0] is str
|
|
357
|
+
assert args[1] in (str, int, float, bool)
|
|
358
|
+
return DictEnvParser()
|
|
359
|
+
if origin is Literal:
|
|
360
|
+
args = cast(tuple[str, ...], get_args(target_type))
|
|
361
|
+
return LiteralEnvParser(args)
|
|
362
|
+
if origin and issubclass(origin, BaseModel):
|
|
363
|
+
target_type = origin
|
|
364
|
+
if issubclass(target_type, DiscriminatedUnionMixin) and (
|
|
365
|
+
inspect.isabstract(target_type) or ABC in target_type.__bases__
|
|
366
|
+
):
|
|
367
|
+
delayed = DelayedParser()
|
|
368
|
+
parsers[target_type] = delayed # Prevent circular dependency
|
|
369
|
+
sub_parsers = {
|
|
370
|
+
c.__name__: get_env_parser(c, parsers)
|
|
371
|
+
for c in get_known_concrete_subclasses(target_type)
|
|
372
|
+
}
|
|
373
|
+
parser = DiscriminatedUnionEnvParser(sub_parsers)
|
|
374
|
+
delayed.parser = parser
|
|
375
|
+
parsers[target_type] = parser
|
|
376
|
+
return parser
|
|
377
|
+
if issubclass(target_type, BaseModel): # type: ignore
|
|
378
|
+
delayed = DelayedParser()
|
|
379
|
+
parsers[target_type] = delayed # Prevent circular dependency
|
|
380
|
+
field_parsers = {}
|
|
381
|
+
descriptions = {}
|
|
382
|
+
for name, field in target_type.model_fields.items():
|
|
383
|
+
field_parsers[name] = get_env_parser(field.annotation, parsers) # type: ignore
|
|
384
|
+
description = field.description
|
|
385
|
+
if description:
|
|
386
|
+
descriptions[name] = description
|
|
387
|
+
|
|
388
|
+
parser = ModelEnvParser(field_parsers, descriptions)
|
|
389
|
+
delayed.parser = parser
|
|
390
|
+
parsers[target_type] = parser
|
|
391
|
+
return parser
|
|
392
|
+
if issubclass(target_type, Enum):
|
|
393
|
+
values = tuple(e.value for e in target_type)
|
|
394
|
+
return LiteralEnvParser(values)
|
|
395
|
+
raise ValueError(f"unknown_type:{target_type}")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _get_default_parsers() -> dict[type, EnvParser]:
|
|
399
|
+
return {
|
|
400
|
+
str: StrEnvParser(),
|
|
401
|
+
int: IntEnvParser(),
|
|
402
|
+
float: FloatEnvParser(),
|
|
403
|
+
bool: BoolEnvParser(),
|
|
404
|
+
type(None): NoneEnvParser(),
|
|
405
|
+
UUID: StrEnvParser(),
|
|
406
|
+
Path: StrEnvParser(),
|
|
407
|
+
datetime: StrEnvParser(),
|
|
408
|
+
SecretStr: StrEnvParser(),
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _create_sample(type_: type):
|
|
413
|
+
if type_ is None:
|
|
414
|
+
return None
|
|
415
|
+
if type_ is str:
|
|
416
|
+
return "..."
|
|
417
|
+
if type_ is int:
|
|
418
|
+
return 0
|
|
419
|
+
if type_ is float:
|
|
420
|
+
return 0.0
|
|
421
|
+
if type_ is bool:
|
|
422
|
+
return False
|
|
423
|
+
try:
|
|
424
|
+
if issubclass(type_, Enum):
|
|
425
|
+
return next(iter(type_))
|
|
426
|
+
except Exception:
|
|
427
|
+
pass
|
|
428
|
+
# Try to initialize and raise exception if failure.
|
|
429
|
+
return type_()
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def from_env(
|
|
433
|
+
target_type: type,
|
|
434
|
+
prefix: str = "",
|
|
435
|
+
parsers: dict[type, EnvParser] | None = None,
|
|
436
|
+
):
|
|
437
|
+
if parsers is None:
|
|
438
|
+
parsers = _get_default_parsers()
|
|
439
|
+
parser = get_env_parser(target_type, parsers)
|
|
440
|
+
json_data = parser.from_env(prefix)
|
|
441
|
+
if json_data is MISSING:
|
|
442
|
+
result = target_type()
|
|
443
|
+
else:
|
|
444
|
+
json_str = json.dumps(json_data)
|
|
445
|
+
type_adapter = TypeAdapter(target_type)
|
|
446
|
+
result = type_adapter.validate_json(json_str)
|
|
447
|
+
return result
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def to_env(
|
|
451
|
+
value: Any,
|
|
452
|
+
prefix: str = "",
|
|
453
|
+
parsers: dict[type, EnvParser] | None = None,
|
|
454
|
+
) -> str:
|
|
455
|
+
if parsers is None:
|
|
456
|
+
parsers = _get_default_parsers()
|
|
457
|
+
parser = get_env_parser(value.__class__, parsers)
|
|
458
|
+
output = StringIO()
|
|
459
|
+
parser.to_env(prefix, value, output)
|
|
460
|
+
return output.getvalue()
|