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.

Files changed (109) hide show
  1. sky/__init__.py +4 -2
  2. sky/backends/backend_utils.py +23 -13
  3. sky/backends/cloud_vm_ray_backend.py +19 -11
  4. sky/catalog/__init__.py +3 -1
  5. sky/catalog/aws_catalog.py +8 -5
  6. sky/catalog/azure_catalog.py +8 -5
  7. sky/catalog/common.py +8 -2
  8. sky/catalog/cudo_catalog.py +5 -2
  9. sky/catalog/do_catalog.py +4 -1
  10. sky/catalog/fluidstack_catalog.py +5 -2
  11. sky/catalog/gcp_catalog.py +8 -5
  12. sky/catalog/hyperbolic_catalog.py +5 -2
  13. sky/catalog/ibm_catalog.py +8 -5
  14. sky/catalog/lambda_catalog.py +8 -5
  15. sky/catalog/nebius_catalog.py +8 -5
  16. sky/catalog/oci_catalog.py +8 -5
  17. sky/catalog/paperspace_catalog.py +4 -1
  18. sky/catalog/runpod_catalog.py +5 -2
  19. sky/catalog/scp_catalog.py +8 -5
  20. sky/catalog/vast_catalog.py +5 -2
  21. sky/catalog/vsphere_catalog.py +4 -1
  22. sky/client/cli/command.py +25 -2
  23. sky/client/sdk.py +9 -4
  24. sky/clouds/aws.py +12 -7
  25. sky/clouds/azure.py +12 -7
  26. sky/clouds/cloud.py +9 -8
  27. sky/clouds/cudo.py +13 -7
  28. sky/clouds/do.py +12 -7
  29. sky/clouds/fluidstack.py +11 -6
  30. sky/clouds/gcp.py +12 -7
  31. sky/clouds/hyperbolic.py +11 -6
  32. sky/clouds/ibm.py +11 -6
  33. sky/clouds/kubernetes.py +7 -3
  34. sky/clouds/lambda_cloud.py +11 -6
  35. sky/clouds/nebius.py +12 -7
  36. sky/clouds/oci.py +12 -7
  37. sky/clouds/paperspace.py +12 -7
  38. sky/clouds/runpod.py +12 -7
  39. sky/clouds/scp.py +11 -6
  40. sky/clouds/vast.py +12 -7
  41. sky/clouds/vsphere.py +11 -6
  42. sky/core.py +6 -1
  43. sky/dashboard/out/404.html +1 -1
  44. sky/dashboard/out/_next/static/chunks/{1043-734e57d2b27dfe5d.js → 1043-869d9c78bf5dd3df.js} +1 -1
  45. sky/dashboard/out/_next/static/chunks/1871-a821dcaaae2a3823.js +6 -0
  46. sky/dashboard/out/_next/static/chunks/{2641.35edc9ccaeaad9e3.js → 2641.5233e938f14e31a7.js} +1 -1
  47. sky/dashboard/out/_next/static/chunks/{4725.4c849b1e05c8e9ad.js → 4725.66125dcd9832aa5d.js} +1 -1
  48. sky/dashboard/out/_next/static/chunks/4869.c7c055a5c2814f33.js +16 -0
  49. sky/dashboard/out/_next/static/chunks/938-63fc419cb82ad9b3.js +1 -0
  50. sky/dashboard/out/_next/static/chunks/9470-8178183f3bae198f.js +1 -0
  51. sky/dashboard/out/_next/static/chunks/pages/_app-507712f30cd3cec3.js +20 -0
  52. sky/dashboard/out/_next/static/chunks/webpack-26cdc782eed15a7d.js +1 -0
  53. sky/dashboard/out/_next/static/css/5122cb0a08486fd3.css +3 -0
  54. sky/dashboard/out/_next/static/{FUjweqdImyeYhMYFON-Se → pTQKG61ng32Zc7gsAROFJ}/_buildManifest.js +1 -1
  55. sky/dashboard/out/clusters/[cluster]/[job].html +1 -1
  56. sky/dashboard/out/clusters/[cluster].html +1 -1
  57. sky/dashboard/out/clusters.html +1 -1
  58. sky/dashboard/out/config.html +1 -1
  59. sky/dashboard/out/index.html +1 -1
  60. sky/dashboard/out/infra/[context].html +1 -1
  61. sky/dashboard/out/infra.html +1 -1
  62. sky/dashboard/out/jobs/[job].html +1 -1
  63. sky/dashboard/out/jobs.html +1 -1
  64. sky/dashboard/out/users.html +1 -1
  65. sky/dashboard/out/volumes.html +1 -1
  66. sky/dashboard/out/workspace/new.html +1 -1
  67. sky/dashboard/out/workspaces/[name].html +1 -1
  68. sky/dashboard/out/workspaces.html +1 -1
  69. sky/global_user_state.py +13 -143
  70. sky/jobs/state.py +9 -88
  71. sky/jobs/utils.py +28 -13
  72. sky/schemas/db/README +4 -0
  73. sky/schemas/db/env.py +90 -0
  74. sky/schemas/db/global_user_state/001_initial_schema.py +124 -0
  75. sky/schemas/db/script.py.mako +28 -0
  76. sky/schemas/db/skypilot_config/001_initial_schema.py +30 -0
  77. sky/schemas/db/spot_jobs/001_initial_schema.py +97 -0
  78. sky/serve/client/sdk.py +6 -2
  79. sky/serve/controller.py +7 -3
  80. sky/serve/serve_state.py +1 -1
  81. sky/serve/serve_utils.py +171 -75
  82. sky/serve/server/core.py +17 -6
  83. sky/server/requests/payloads.py +2 -0
  84. sky/server/requests/requests.py +1 -1
  85. sky/setup_files/MANIFEST.in +2 -0
  86. sky/setup_files/alembic.ini +152 -0
  87. sky/setup_files/dependencies.py +1 -0
  88. sky/skylet/configs.py +1 -1
  89. sky/skylet/job_lib.py +1 -1
  90. sky/skypilot_config.py +32 -6
  91. sky/users/permission.py +1 -1
  92. sky/utils/common_utils.py +77 -0
  93. sky/utils/db/__init__.py +0 -0
  94. sky/utils/{db_utils.py → db/db_utils.py} +59 -0
  95. sky/utils/db/migration_utils.py +53 -0
  96. {skypilot_nightly-1.0.0.dev20250718.dist-info → skypilot_nightly-1.0.0.dev20250720.dist-info}/METADATA +2 -1
  97. {skypilot_nightly-1.0.0.dev20250718.dist-info → skypilot_nightly-1.0.0.dev20250720.dist-info}/RECORD +102 -93
  98. sky/dashboard/out/_next/static/chunks/1871-76491ac174a95278.js +0 -6
  99. sky/dashboard/out/_next/static/chunks/4869.bdd42f14b51d1d6f.js +0 -16
  100. sky/dashboard/out/_next/static/chunks/938-6a9ffdaa21eee969.js +0 -1
  101. sky/dashboard/out/_next/static/chunks/9470-b6f6a35283863a6f.js +0 -1
  102. sky/dashboard/out/_next/static/chunks/pages/_app-771a40cde532309b.js +0 -20
  103. sky/dashboard/out/_next/static/chunks/webpack-6b0575ea521af4f3.js +0 -1
  104. sky/dashboard/out/_next/static/css/219887b94512388c.css +0 -3
  105. /sky/dashboard/out/_next/static/{FUjweqdImyeYhMYFON-Se → pTQKG61ng32Zc7gsAROFJ}/_ssgManifest.js +0 -0
  106. {skypilot_nightly-1.0.0.dev20250718.dist-info → skypilot_nightly-1.0.0.dev20250720.dist-info}/WHEEL +0 -0
  107. {skypilot_nightly-1.0.0.dev20250718.dist-info → skypilot_nightly-1.0.0.dev20250720.dist-info}/entry_points.txt +0 -0
  108. {skypilot_nightly-1.0.0.dev20250718.dist-info → skypilot_nightly-1.0.0.dev20250720.dist-info}/licenses/LICENSE +0 -0
  109. {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
@@ -68,6 +68,7 @@ install_requires = [
68
68
  'pyjwt',
69
69
  'gitpython',
70
70
  'types-paramiko',
71
+ 'alembic',
71
72
  ]
72
73
 
73
74
  server_dependencies = [
sky/skylet/configs.py CHANGED
@@ -5,7 +5,7 @@ import pathlib
5
5
  import threading
6
6
  from typing import Callable, Optional, Union
7
7
 
8
- from sky.utils import db_utils
8
+ from sky.utils.db import db_utils
9
9
 
10
10
  _DB_PATH = None
11
11
  _db_init_lock = threading.Lock()
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 _DB_USE_LOCK:
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
- db_utils.add_tables_to_db_sqlalchemy(Base.metadata,
578
- sqlalchemy_engine)
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
- db_utils.add_tables_to_db_sqlalchemy(Base.metadata,
867
- sqlalchemy_engine)
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
 
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.dev20250718
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"