skypilot-nightly 1.0.0.dev20250718__py3-none-any.whl → 1.0.0.dev20250720__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.
Potentially problematic release.
This version of skypilot-nightly might be problematic. Click here for more details.
- sky/__init__.py +4 -2
- sky/backends/backend_utils.py +23 -13
- sky/backends/cloud_vm_ray_backend.py +19 -11
- sky/catalog/__init__.py +3 -1
- sky/catalog/aws_catalog.py +8 -5
- sky/catalog/azure_catalog.py +8 -5
- sky/catalog/common.py +8 -2
- sky/catalog/cudo_catalog.py +5 -2
- sky/catalog/do_catalog.py +4 -1
- sky/catalog/fluidstack_catalog.py +5 -2
- sky/catalog/gcp_catalog.py +8 -5
- sky/catalog/hyperbolic_catalog.py +5 -2
- sky/catalog/ibm_catalog.py +8 -5
- sky/catalog/lambda_catalog.py +8 -5
- sky/catalog/nebius_catalog.py +8 -5
- sky/catalog/oci_catalog.py +8 -5
- sky/catalog/paperspace_catalog.py +4 -1
- sky/catalog/runpod_catalog.py +5 -2
- sky/catalog/scp_catalog.py +8 -5
- sky/catalog/vast_catalog.py +5 -2
- sky/catalog/vsphere_catalog.py +4 -1
- sky/client/cli/command.py +25 -2
- sky/client/sdk.py +9 -4
- sky/clouds/aws.py +12 -7
- sky/clouds/azure.py +12 -7
- sky/clouds/cloud.py +9 -8
- sky/clouds/cudo.py +13 -7
- sky/clouds/do.py +12 -7
- sky/clouds/fluidstack.py +11 -6
- sky/clouds/gcp.py +12 -7
- sky/clouds/hyperbolic.py +11 -6
- sky/clouds/ibm.py +11 -6
- sky/clouds/kubernetes.py +7 -3
- sky/clouds/lambda_cloud.py +11 -6
- sky/clouds/nebius.py +12 -7
- sky/clouds/oci.py +12 -7
- sky/clouds/paperspace.py +12 -7
- sky/clouds/runpod.py +12 -7
- sky/clouds/scp.py +11 -6
- sky/clouds/vast.py +12 -7
- sky/clouds/vsphere.py +11 -6
- sky/core.py +6 -1
- sky/dashboard/out/404.html +1 -1
- sky/dashboard/out/_next/static/chunks/{1043-734e57d2b27dfe5d.js → 1043-869d9c78bf5dd3df.js} +1 -1
- sky/dashboard/out/_next/static/chunks/1871-a821dcaaae2a3823.js +6 -0
- sky/dashboard/out/_next/static/chunks/{2641.35edc9ccaeaad9e3.js → 2641.5233e938f14e31a7.js} +1 -1
- sky/dashboard/out/_next/static/chunks/{4725.4c849b1e05c8e9ad.js → 4725.66125dcd9832aa5d.js} +1 -1
- sky/dashboard/out/_next/static/chunks/4869.c7c055a5c2814f33.js +16 -0
- sky/dashboard/out/_next/static/chunks/938-63fc419cb82ad9b3.js +1 -0
- sky/dashboard/out/_next/static/chunks/9470-8178183f3bae198f.js +1 -0
- sky/dashboard/out/_next/static/chunks/pages/_app-507712f30cd3cec3.js +20 -0
- sky/dashboard/out/_next/static/chunks/webpack-26cdc782eed15a7d.js +1 -0
- sky/dashboard/out/_next/static/css/5122cb0a08486fd3.css +3 -0
- sky/dashboard/out/_next/static/{FUjweqdImyeYhMYFON-Se → pTQKG61ng32Zc7gsAROFJ}/_buildManifest.js +1 -1
- sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
- sky/dashboard/out/clusters/[cluster].html +1 -1
- sky/dashboard/out/clusters.html +1 -1
- sky/dashboard/out/config.html +1 -1
- sky/dashboard/out/index.html +1 -1
- sky/dashboard/out/infra/[context].html +1 -1
- sky/dashboard/out/infra.html +1 -1
- sky/dashboard/out/jobs/[job].html +1 -1
- sky/dashboard/out/jobs.html +1 -1
- sky/dashboard/out/users.html +1 -1
- sky/dashboard/out/volumes.html +1 -1
- sky/dashboard/out/workspace/new.html +1 -1
- sky/dashboard/out/workspaces/[name].html +1 -1
- sky/dashboard/out/workspaces.html +1 -1
- sky/global_user_state.py +13 -143
- sky/jobs/state.py +9 -88
- sky/jobs/utils.py +28 -13
- sky/schemas/db/README +4 -0
- sky/schemas/db/env.py +90 -0
- sky/schemas/db/global_user_state/001_initial_schema.py +124 -0
- sky/schemas/db/script.py.mako +28 -0
- sky/schemas/db/skypilot_config/001_initial_schema.py +30 -0
- sky/schemas/db/spot_jobs/001_initial_schema.py +97 -0
- sky/serve/client/sdk.py +6 -2
- sky/serve/controller.py +7 -3
- sky/serve/serve_state.py +1 -1
- sky/serve/serve_utils.py +171 -75
- sky/serve/server/core.py +17 -6
- sky/server/requests/payloads.py +2 -0
- sky/server/requests/requests.py +1 -1
- sky/setup_files/MANIFEST.in +2 -0
- sky/setup_files/alembic.ini +152 -0
- sky/setup_files/dependencies.py +1 -0
- sky/skylet/configs.py +1 -1
- sky/skylet/job_lib.py +1 -1
- sky/skypilot_config.py +32 -6
- sky/users/permission.py +1 -1
- sky/utils/common_utils.py +77 -0
- sky/utils/db/__init__.py +0 -0
- sky/utils/{db_utils.py → db/db_utils.py} +59 -0
- sky/utils/db/migration_utils.py +53 -0
- {skypilot_nightly-1.0.0.dev20250718.dist-info → skypilot_nightly-1.0.0.dev20250720.dist-info}/METADATA +2 -1
- {skypilot_nightly-1.0.0.dev20250718.dist-info → skypilot_nightly-1.0.0.dev20250720.dist-info}/RECORD +102 -93
- sky/dashboard/out/_next/static/chunks/1871-76491ac174a95278.js +0 -6
- sky/dashboard/out/_next/static/chunks/4869.bdd42f14b51d1d6f.js +0 -16
- sky/dashboard/out/_next/static/chunks/938-6a9ffdaa21eee969.js +0 -1
- sky/dashboard/out/_next/static/chunks/9470-b6f6a35283863a6f.js +0 -1
- sky/dashboard/out/_next/static/chunks/pages/_app-771a40cde532309b.js +0 -20
- sky/dashboard/out/_next/static/chunks/webpack-6b0575ea521af4f3.js +0 -1
- sky/dashboard/out/_next/static/css/219887b94512388c.css +0 -3
- /sky/dashboard/out/_next/static/{FUjweqdImyeYhMYFON-Se → pTQKG61ng32Zc7gsAROFJ}/_ssgManifest.js +0 -0
- {skypilot_nightly-1.0.0.dev20250718.dist-info → skypilot_nightly-1.0.0.dev20250720.dist-info}/WHEEL +0 -0
- {skypilot_nightly-1.0.0.dev20250718.dist-info → skypilot_nightly-1.0.0.dev20250720.dist-info}/entry_points.txt +0 -0
- {skypilot_nightly-1.0.0.dev20250718.dist-info → skypilot_nightly-1.0.0.dev20250720.dist-info}/licenses/LICENSE +0 -0
- {skypilot_nightly-1.0.0.dev20250718.dist-info → skypilot_nightly-1.0.0.dev20250720.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# alembic configuration for global user state, jobs state, and sky config db migrations.
|
|
2
|
+
|
|
3
|
+
[DEFAULT]
|
|
4
|
+
# path to migration scripts.
|
|
5
|
+
# this is typically a path given in POSIX (e.g. forward slashes)
|
|
6
|
+
# format, relative to the token %(here)s which refers to the location of this
|
|
7
|
+
# ini file
|
|
8
|
+
script_location = %(here)s/../schemas/db
|
|
9
|
+
|
|
10
|
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
|
11
|
+
# Uncomment the line below if you want the files to be prepended with date and time
|
|
12
|
+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
|
13
|
+
# for all available tokens
|
|
14
|
+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
|
15
|
+
|
|
16
|
+
# sys.path path, will be prepended to sys.path if present.
|
|
17
|
+
# defaults to the current working directory. for multiple paths, the path separator
|
|
18
|
+
# is defined by "path_separator" below.
|
|
19
|
+
prepend_sys_path = .
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# timezone to use when rendering the date within the migration file
|
|
23
|
+
# as well as the filename.
|
|
24
|
+
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
|
|
25
|
+
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
|
26
|
+
# string value is passed to ZoneInfo()
|
|
27
|
+
# leave blank for localtime
|
|
28
|
+
# timezone =
|
|
29
|
+
|
|
30
|
+
# max length of characters to apply to the "slug" field
|
|
31
|
+
# truncate_slug_length = 40
|
|
32
|
+
|
|
33
|
+
# set to 'true' to run the environment during
|
|
34
|
+
# the 'revision' command, regardless of autogenerate
|
|
35
|
+
# revision_environment = false
|
|
36
|
+
|
|
37
|
+
# set to 'true' to allow .pyc and .pyo files without
|
|
38
|
+
# a source .py file to be detected as revisions in the
|
|
39
|
+
# versions/ directory
|
|
40
|
+
# sourceless = false
|
|
41
|
+
|
|
42
|
+
# version location specification; This defaults
|
|
43
|
+
# to <script_location>/versions. When using multiple version
|
|
44
|
+
# directories, initial revisions must be specified with --version-path.
|
|
45
|
+
# The path separator used here should be the separator specified by "path_separator"
|
|
46
|
+
# below.
|
|
47
|
+
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
|
48
|
+
|
|
49
|
+
# path_separator; This indicates what character is used to split lists of file
|
|
50
|
+
# paths, including version_locations and prepend_sys_path within configparser
|
|
51
|
+
# files such as alembic.ini.
|
|
52
|
+
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
|
53
|
+
# to provide os-dependent path splitting.
|
|
54
|
+
#
|
|
55
|
+
# Note that in order to support legacy alembic.ini files, this default does NOT
|
|
56
|
+
# take place if path_separator is not present in alembic.ini. If this
|
|
57
|
+
# option is omitted entirely, fallback logic is as follows:
|
|
58
|
+
#
|
|
59
|
+
# 1. Parsing of the version_locations option falls back to using the legacy
|
|
60
|
+
# "version_path_separator" key, which if absent then falls back to the legacy
|
|
61
|
+
# behavior of splitting on spaces and/or commas.
|
|
62
|
+
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
|
63
|
+
# behavior of splitting on spaces, commas, or colons.
|
|
64
|
+
#
|
|
65
|
+
# Valid values for path_separator are:
|
|
66
|
+
#
|
|
67
|
+
# path_separator = :
|
|
68
|
+
# path_separator = ;
|
|
69
|
+
# path_separator = space
|
|
70
|
+
# path_separator = newline
|
|
71
|
+
#
|
|
72
|
+
# Use os.pathsep. Default configuration used for new projects.
|
|
73
|
+
path_separator = os
|
|
74
|
+
|
|
75
|
+
# set to 'true' to search source files recursively
|
|
76
|
+
# in each "version_locations" directory
|
|
77
|
+
# new in Alembic version 1.10
|
|
78
|
+
# recursive_version_locations = false
|
|
79
|
+
|
|
80
|
+
# the output encoding used when revision files
|
|
81
|
+
# are written from script.py.mako
|
|
82
|
+
# output_encoding = utf-8
|
|
83
|
+
|
|
84
|
+
# database URL. This is consumed by the user-maintained env.py script only.
|
|
85
|
+
# other means of configuring database URLs may be customized within the env.py
|
|
86
|
+
# file.
|
|
87
|
+
# sqlalchemy.url = driver://user:pass@localhost/dbname
|
|
88
|
+
|
|
89
|
+
[state_db]
|
|
90
|
+
version_locations = %(here)s/../schemas/db/global_user_state
|
|
91
|
+
version_table = alembic_version_state_db
|
|
92
|
+
|
|
93
|
+
[spot_jobs_db]
|
|
94
|
+
version_locations = %(here)s/../schemas/db/spot_jobs
|
|
95
|
+
version_table = alembic_version_spot_jobs_db
|
|
96
|
+
|
|
97
|
+
[sky_config_db]
|
|
98
|
+
version_locations = %(here)s/../schemas/db/skypilot_config
|
|
99
|
+
version_table = alembic_version_sky_config_db
|
|
100
|
+
|
|
101
|
+
[post_write_hooks]
|
|
102
|
+
# post_write_hooks defines scripts or Python functions that are run
|
|
103
|
+
# on newly generated revision scripts. See the documentation for further
|
|
104
|
+
# detail and examples
|
|
105
|
+
|
|
106
|
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
|
107
|
+
# hooks = black
|
|
108
|
+
# black.type = console_scripts
|
|
109
|
+
# black.entrypoint = black
|
|
110
|
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
|
111
|
+
|
|
112
|
+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
|
113
|
+
# hooks = ruff
|
|
114
|
+
# ruff.type = exec
|
|
115
|
+
# ruff.executable = %(here)s/.venv/bin/ruff
|
|
116
|
+
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
|
117
|
+
|
|
118
|
+
# Logging configuration. This is also consumed by the user-maintained
|
|
119
|
+
# env.py script only.
|
|
120
|
+
[loggers]
|
|
121
|
+
keys = root,sqlalchemy,alembic
|
|
122
|
+
|
|
123
|
+
[handlers]
|
|
124
|
+
keys = console
|
|
125
|
+
|
|
126
|
+
[formatters]
|
|
127
|
+
keys = generic
|
|
128
|
+
|
|
129
|
+
[logger_root]
|
|
130
|
+
level = WARNING
|
|
131
|
+
handlers = console
|
|
132
|
+
qualname =
|
|
133
|
+
|
|
134
|
+
[logger_sqlalchemy]
|
|
135
|
+
level = WARNING
|
|
136
|
+
handlers =
|
|
137
|
+
qualname = sqlalchemy.engine
|
|
138
|
+
|
|
139
|
+
[logger_alembic]
|
|
140
|
+
level = WARNING
|
|
141
|
+
handlers =
|
|
142
|
+
qualname = alembic
|
|
143
|
+
|
|
144
|
+
[handler_console]
|
|
145
|
+
class = StreamHandler
|
|
146
|
+
args = (sys.stderr,)
|
|
147
|
+
level = NOTSET
|
|
148
|
+
formatter = generic
|
|
149
|
+
|
|
150
|
+
[formatter_generic]
|
|
151
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
152
|
+
datefmt = %H:%M:%S
|
sky/setup_files/dependencies.py
CHANGED
sky/skylet/configs.py
CHANGED
sky/skylet/job_lib.py
CHANGED
|
@@ -24,10 +24,10 @@ from sky import sky_logging
|
|
|
24
24
|
from sky.adaptors import common as adaptors_common
|
|
25
25
|
from sky.skylet import constants
|
|
26
26
|
from sky.utils import common_utils
|
|
27
|
-
from sky.utils import db_utils
|
|
28
27
|
from sky.utils import log_utils
|
|
29
28
|
from sky.utils import message_utils
|
|
30
29
|
from sky.utils import subprocess_utils
|
|
30
|
+
from sky.utils.db import db_utils
|
|
31
31
|
|
|
32
32
|
if typing.TYPE_CHECKING:
|
|
33
33
|
import psutil
|
sky/skypilot_config.py
CHANGED
|
@@ -58,8 +58,10 @@ import threading
|
|
|
58
58
|
import typing
|
|
59
59
|
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
|
60
60
|
|
|
61
|
+
from alembic import command as alembic_command
|
|
61
62
|
import filelock
|
|
62
63
|
import sqlalchemy
|
|
64
|
+
from sqlalchemy import exc as sqlalchemy_exc
|
|
63
65
|
from sqlalchemy import orm
|
|
64
66
|
from sqlalchemy.dialects import postgresql
|
|
65
67
|
from sqlalchemy.dialects import sqlite
|
|
@@ -73,9 +75,10 @@ from sky.skylet import constants
|
|
|
73
75
|
from sky.utils import common_utils
|
|
74
76
|
from sky.utils import config_utils
|
|
75
77
|
from sky.utils import context
|
|
76
|
-
from sky.utils import db_utils
|
|
77
78
|
from sky.utils import schemas
|
|
78
79
|
from sky.utils import ux_utils
|
|
80
|
+
from sky.utils.db import db_utils
|
|
81
|
+
from sky.utils.db import migration_utils
|
|
79
82
|
from sky.utils.kubernetes import config_map_utils
|
|
80
83
|
|
|
81
84
|
if typing.TYPE_CHECKING:
|
|
@@ -571,11 +574,17 @@ def _reload_config_as_server() -> None:
|
|
|
571
574
|
'if db config is specified, no other config is allowed')
|
|
572
575
|
|
|
573
576
|
if db_url:
|
|
574
|
-
with
|
|
577
|
+
with migration_utils.db_lock(migration_utils.SKYPILOT_CONFIG_DB_NAME):
|
|
575
578
|
sqlalchemy_engine = sqlalchemy.create_engine(db_url,
|
|
576
579
|
poolclass=NullPool)
|
|
577
|
-
|
|
578
|
-
|
|
580
|
+
|
|
581
|
+
# Get alembic config for sky config db and run migrations
|
|
582
|
+
alembic_config = migration_utils.get_alembic_config(
|
|
583
|
+
sqlalchemy_engine, migration_utils.SKYPILOT_CONFIG_DB_NAME)
|
|
584
|
+
# pylint: disable=line-too-long
|
|
585
|
+
alembic_config.config_ini_section = migration_utils.SKYPILOT_CONFIG_DB_NAME
|
|
586
|
+
alembic_command.upgrade(alembic_config,
|
|
587
|
+
migration_utils.SKYPILOT_CONFIG_VERSION)
|
|
579
588
|
|
|
580
589
|
def _get_config_yaml_from_db(
|
|
581
590
|
key: str) -> Optional[config_utils.Config]:
|
|
@@ -863,8 +872,25 @@ def update_api_server_config_no_lock(config: config_utils.Config) -> None:
|
|
|
863
872
|
with _DB_USE_LOCK:
|
|
864
873
|
sqlalchemy_engine = sqlalchemy.create_engine(existing_db_url,
|
|
865
874
|
poolclass=NullPool)
|
|
866
|
-
|
|
867
|
-
|
|
875
|
+
|
|
876
|
+
# Get alembic config for sky config db and run migrations
|
|
877
|
+
alembic_config = migration_utils.get_alembic_config(
|
|
878
|
+
sqlalchemy_engine, 'sky_config_db')
|
|
879
|
+
alembic_config.config_ini_section = 'sky_config_db'
|
|
880
|
+
try:
|
|
881
|
+
alembic_command.upgrade(alembic_config, '001')
|
|
882
|
+
except (sqlalchemy_exc.IntegrityError,
|
|
883
|
+
sqlalchemy_exc.OperationalError) as e:
|
|
884
|
+
# If the version already exists (due to concurrent
|
|
885
|
+
# initialization), we can safely ignore this error
|
|
886
|
+
if ('UNIQUE constraint failed: '
|
|
887
|
+
'alembic_version_sky_config_db.version_num'
|
|
888
|
+
in str(e) or
|
|
889
|
+
'table alembic_version_sky_config_db already exists'
|
|
890
|
+
in str(e)):
|
|
891
|
+
pass
|
|
892
|
+
else:
|
|
893
|
+
raise
|
|
868
894
|
|
|
869
895
|
def _set_config_yaml_to_db(key: str,
|
|
870
896
|
config: config_utils.Config):
|
sky/users/permission.py
CHANGED
|
@@ -15,7 +15,7 @@ from sky import sky_logging
|
|
|
15
15
|
from sky.skylet import constants
|
|
16
16
|
from sky.users import rbac
|
|
17
17
|
from sky.utils import common_utils
|
|
18
|
-
from sky.utils import db_utils
|
|
18
|
+
from sky.utils.db import db_utils
|
|
19
19
|
|
|
20
20
|
logging.getLogger('casbin.policy').setLevel(sky_logging.ERROR)
|
|
21
21
|
logging.getLogger('casbin.role').setLevel(sky_logging.ERROR)
|
sky/utils/common_utils.py
CHANGED
|
@@ -369,6 +369,83 @@ def get_pretty_entrypoint_cmd() -> str:
|
|
|
369
369
|
return ' '.join(argv)
|
|
370
370
|
|
|
371
371
|
|
|
372
|
+
def read_last_n_lines(file_path: str,
|
|
373
|
+
n: int,
|
|
374
|
+
chunk_size: int = 8192,
|
|
375
|
+
encoding: str = 'utf-8',
|
|
376
|
+
errors: str = 'replace') -> List[str]:
|
|
377
|
+
"""Read the last N lines of a file.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
file_path: Path to the file to read.
|
|
381
|
+
n: Number of lines to read from the end of the file.
|
|
382
|
+
chunk_size: Size of chunks in bytes.
|
|
383
|
+
encoding: Encoding to use when decoding binary chunks.
|
|
384
|
+
errors: Error handling for decode errors (e.g., 'replace', 'ignore').
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
A list of the last N lines, preserving newlines where applicable.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
assert n >= 0, f'n must be non-negative. Got {n}'
|
|
391
|
+
assert chunk_size > 0, f'chunk_size must be positive. Got {chunk_size}'
|
|
392
|
+
assert os.path.exists(file_path), f'File not found: {file_path}'
|
|
393
|
+
|
|
394
|
+
if n == 0:
|
|
395
|
+
return []
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
with open(file_path, 'rb') as f:
|
|
399
|
+
# Start reading from the end of the file
|
|
400
|
+
f.seek(0, os.SEEK_END)
|
|
401
|
+
file_size = f.tell()
|
|
402
|
+
if file_size == 0:
|
|
403
|
+
return []
|
|
404
|
+
|
|
405
|
+
pos = file_size
|
|
406
|
+
lines_found = 0
|
|
407
|
+
chunks = []
|
|
408
|
+
|
|
409
|
+
# Read backwards in chunks until we've found at least n newlines
|
|
410
|
+
while pos > 0 and lines_found <= n:
|
|
411
|
+
read_size = min(chunk_size, pos)
|
|
412
|
+
pos -= read_size
|
|
413
|
+
f.seek(pos)
|
|
414
|
+
chunk = f.read(read_size)
|
|
415
|
+
chunks.append(chunk)
|
|
416
|
+
lines_found += chunk.count(b'\n')
|
|
417
|
+
|
|
418
|
+
# Combine all chunks in reverse order since we read backwards
|
|
419
|
+
full_bytes = b''.join(reversed(chunks))
|
|
420
|
+
|
|
421
|
+
# Split by newline byte. Note: this handles '\n' endings.
|
|
422
|
+
all_lines = full_bytes.split(b'\n')
|
|
423
|
+
|
|
424
|
+
# Handle edge case: if file ends with a newline, last element is b''
|
|
425
|
+
if all_lines and all_lines[-1] == b'':
|
|
426
|
+
result_bytes = all_lines[-n - 1:-1]
|
|
427
|
+
else:
|
|
428
|
+
result_bytes = all_lines[-n:]
|
|
429
|
+
|
|
430
|
+
# Decode each line and normalize CR/LF endings
|
|
431
|
+
decoded_lines = [
|
|
432
|
+
line.decode(encoding, errors=errors).rstrip('\r') + '\n'
|
|
433
|
+
for line in result_bytes[:-1]
|
|
434
|
+
]
|
|
435
|
+
|
|
436
|
+
# Decode the final line — only add newline if it was present
|
|
437
|
+
last_line = result_bytes[-1].decode(encoding,
|
|
438
|
+
errors=errors).rstrip('\r')
|
|
439
|
+
decoded_lines.append(last_line)
|
|
440
|
+
|
|
441
|
+
return decoded_lines
|
|
442
|
+
|
|
443
|
+
except OSError as e:
|
|
444
|
+
with ux_utils.print_exception_no_traceback():
|
|
445
|
+
raise RuntimeError(
|
|
446
|
+
f'Failed to read last {n} lines from {file_path}: {e}') from e
|
|
447
|
+
|
|
448
|
+
|
|
372
449
|
def _redact_secrets_values(argv: List[str]) -> List[str]:
|
|
373
450
|
"""Redact sensitive values from --secret arguments.
|
|
374
451
|
|
sky/utils/db/__init__.py
ADDED
|
File without changes
|
|
@@ -9,6 +9,9 @@ from typing import Any, Callable, Optional
|
|
|
9
9
|
import sqlalchemy
|
|
10
10
|
from sqlalchemy import exc as sqlalchemy_exc
|
|
11
11
|
|
|
12
|
+
from sky import sky_logging
|
|
13
|
+
|
|
14
|
+
logger = sky_logging.init_logger(__name__)
|
|
12
15
|
if typing.TYPE_CHECKING:
|
|
13
16
|
from sqlalchemy.orm import Session
|
|
14
17
|
|
|
@@ -146,6 +149,62 @@ def add_column_to_table_sqlalchemy(
|
|
|
146
149
|
session.commit()
|
|
147
150
|
|
|
148
151
|
|
|
152
|
+
def add_column_to_table_alembic(
|
|
153
|
+
table_name: str,
|
|
154
|
+
column_name: str,
|
|
155
|
+
column_type: sqlalchemy.types.TypeEngine,
|
|
156
|
+
server_default: Optional[str] = None,
|
|
157
|
+
copy_from: Optional[str] = None,
|
|
158
|
+
value_to_replace_existing_entries: Optional[Any] = None,
|
|
159
|
+
):
|
|
160
|
+
"""Add a column to a table using Alembic operations.
|
|
161
|
+
|
|
162
|
+
This provides the same interface as add_column_to_table_sqlalchemy but
|
|
163
|
+
uses Alembic's connection context for proper migration support.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
table_name: Name of the table to add column to
|
|
167
|
+
column_name: Name of the new column
|
|
168
|
+
column_type: SQLAlchemy column type
|
|
169
|
+
server_default: Server-side default value for the column
|
|
170
|
+
copy_from: Column name to copy values from (for existing rows)
|
|
171
|
+
value_to_replace_existing_entries: Default value for existing NULL
|
|
172
|
+
entries
|
|
173
|
+
"""
|
|
174
|
+
from alembic import op # pylint: disable=import-outside-toplevel
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
# Create the column with server_default if provided
|
|
178
|
+
column = sqlalchemy.Column(column_name,
|
|
179
|
+
column_type,
|
|
180
|
+
server_default=server_default)
|
|
181
|
+
op.add_column(table_name, column)
|
|
182
|
+
|
|
183
|
+
# Handle data migration
|
|
184
|
+
if copy_from is not None:
|
|
185
|
+
op.execute(
|
|
186
|
+
sqlalchemy.text(
|
|
187
|
+
f'UPDATE {table_name} SET {column_name} = {copy_from}'))
|
|
188
|
+
|
|
189
|
+
if value_to_replace_existing_entries is not None:
|
|
190
|
+
# Use parameterized query for safety
|
|
191
|
+
op.get_bind().execute(
|
|
192
|
+
sqlalchemy.text(f'UPDATE {table_name} '
|
|
193
|
+
f'SET {column_name} = :replacement_value '
|
|
194
|
+
f'WHERE {column_name} IS NULL'),
|
|
195
|
+
{'replacement_value': value_to_replace_existing_entries})
|
|
196
|
+
except sqlalchemy_exc.ProgrammingError as e:
|
|
197
|
+
if 'already exists' in str(e).lower():
|
|
198
|
+
pass # Column already exists, that's fine
|
|
199
|
+
else:
|
|
200
|
+
raise
|
|
201
|
+
except sqlalchemy_exc.OperationalError as e:
|
|
202
|
+
if 'duplicate column name' in str(e).lower():
|
|
203
|
+
pass # Column already exists, that's fine
|
|
204
|
+
else:
|
|
205
|
+
raise
|
|
206
|
+
|
|
207
|
+
|
|
149
208
|
class SQLiteConn(threading.local):
|
|
150
209
|
"""Thread-local connection to the sqlite3 database."""
|
|
151
210
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Constants for the database schemas."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from alembic.config import Config
|
|
7
|
+
import filelock
|
|
8
|
+
import sqlalchemy
|
|
9
|
+
|
|
10
|
+
DB_INIT_LOCK_TIMEOUT_SECONDS = 10
|
|
11
|
+
|
|
12
|
+
GLOBAL_USER_STATE_DB_NAME = 'state_db'
|
|
13
|
+
GLOBAL_USER_STATE_VERSION = '001'
|
|
14
|
+
GLOBAL_USER_STATE_LOCK_PATH = '~/.sky/locks/.state_db.lock'
|
|
15
|
+
|
|
16
|
+
SKYPILOT_CONFIG_DB_NAME = 'skypilot_config_db'
|
|
17
|
+
SKYPILOT_CONFIG_VERSION = '001'
|
|
18
|
+
SKYPILOT_CONFIG_LOCK_PATH = '~/.sky/locks/.skypilot_config_db.lock'
|
|
19
|
+
|
|
20
|
+
SPOT_JOBS_DB_NAME = 'spot_jobs_db'
|
|
21
|
+
SPOT_JOBS_VERSION = '001'
|
|
22
|
+
SPOT_JOBS_LOCK_PATH = '~/.sky/locks/.spot_jobs_db.lock'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@contextlib.contextmanager
|
|
26
|
+
def db_lock(db_name: str):
|
|
27
|
+
lock_path = os.path.expanduser(f'~/.sky/locks/.{db_name}.lock')
|
|
28
|
+
try:
|
|
29
|
+
with filelock.FileLock(lock_path, timeout=DB_INIT_LOCK_TIMEOUT_SECONDS):
|
|
30
|
+
yield
|
|
31
|
+
except filelock.Timeout as e:
|
|
32
|
+
raise RuntimeError(f'Failed to initialize database due to a timeout '
|
|
33
|
+
f'when trying to acquire the lock at '
|
|
34
|
+
f'{lock_path}. '
|
|
35
|
+
'Please try again or manually remove the lock '
|
|
36
|
+
f'file if you believe it is stale.') from e
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_alembic_config(engine: sqlalchemy.engine.Engine, section: str):
|
|
40
|
+
"""Get Alembic configuration for the given section"""
|
|
41
|
+
# Use the alembic.ini file from setup_files (included in wheel)
|
|
42
|
+
# From sky/utils/db/migration_utils.py -> sky/setup_files/alembic.ini
|
|
43
|
+
alembic_ini_path = os.path.join(
|
|
44
|
+
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
|
|
45
|
+
'setup_files', 'alembic.ini')
|
|
46
|
+
alembic_cfg = Config(alembic_ini_path, ini_section=section)
|
|
47
|
+
|
|
48
|
+
# Override the database URL to match SkyPilot's current connection
|
|
49
|
+
# Use render_as_string to get the full URL with password
|
|
50
|
+
url = engine.url.render_as_string(hide_password=False)
|
|
51
|
+
alembic_cfg.set_section_option(section, 'sqlalchemy.url', url)
|
|
52
|
+
|
|
53
|
+
return alembic_cfg
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skypilot-nightly
|
|
3
|
-
Version: 1.0.0.
|
|
3
|
+
Version: 1.0.0.dev20250720
|
|
4
4
|
Summary: SkyPilot: Run AI on Any Infra — Unified, Faster, Cheaper.
|
|
5
5
|
Author: SkyPilot Team
|
|
6
6
|
License: Apache 2.0
|
|
@@ -56,6 +56,7 @@ Requires-Dist: passlib
|
|
|
56
56
|
Requires-Dist: pyjwt
|
|
57
57
|
Requires-Dist: gitpython
|
|
58
58
|
Requires-Dist: types-paramiko
|
|
59
|
+
Requires-Dist: alembic
|
|
59
60
|
Provides-Extra: aws
|
|
60
61
|
Requires-Dist: awscli>=1.27.10; extra == "aws"
|
|
61
62
|
Requires-Dist: botocore>=1.29.10; extra == "aws"
|