meerschaum 2.7.1__py3-none-any.whl → 2.7.3__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. meerschaum/_internal/arguments/_parse_arguments.py +2 -0
  2. meerschaum/_internal/arguments/_parser.py +17 -11
  3. meerschaum/actions/clear.py +1 -1
  4. meerschaum/actions/edit.py +1 -1
  5. meerschaum/actions/verify.py +18 -21
  6. meerschaum/api/dash/__init__.py +0 -1
  7. meerschaum/api/dash/callbacks/dashboard.py +2 -11
  8. meerschaum/api/dash/connectors.py +7 -9
  9. meerschaum/api/resources/templates/termpage.html +3 -3
  10. meerschaum/config/_version.py +1 -1
  11. meerschaum/connectors/sql/_fetch.py +45 -26
  12. meerschaum/connectors/sql/_instance.py +4 -4
  13. meerschaum/connectors/sql/_pipes.py +135 -103
  14. meerschaum/core/Pipe/_attributes.py +1 -1
  15. meerschaum/core/Pipe/_dtypes.py +9 -9
  16. meerschaum/core/Pipe/_fetch.py +2 -3
  17. meerschaum/core/Pipe/_sync.py +11 -3
  18. meerschaum/core/Pipe/_verify.py +9 -5
  19. meerschaum/jobs/__init__.py +1 -1
  20. meerschaum/utils/dataframe.py +10 -2
  21. meerschaum/utils/dtypes/sql.py +1 -1
  22. meerschaum/utils/formatting/__init__.py +5 -25
  23. meerschaum/utils/formatting/_pipes.py +9 -6
  24. meerschaum/utils/sql.py +156 -87
  25. meerschaum/utils/venv/__init__.py +44 -6
  26. {meerschaum-2.7.1.dist-info → meerschaum-2.7.3.dist-info}/METADATA +1 -1
  27. {meerschaum-2.7.1.dist-info → meerschaum-2.7.3.dist-info}/RECORD +33 -33
  28. {meerschaum-2.7.1.dist-info → meerschaum-2.7.3.dist-info}/LICENSE +0 -0
  29. {meerschaum-2.7.1.dist-info → meerschaum-2.7.3.dist-info}/NOTICE +0 -0
  30. {meerschaum-2.7.1.dist-info → meerschaum-2.7.3.dist-info}/WHEEL +0 -0
  31. {meerschaum-2.7.1.dist-info → meerschaum-2.7.3.dist-info}/entry_points.txt +0 -0
  32. {meerschaum-2.7.1.dist-info → meerschaum-2.7.3.dist-info}/top_level.txt +0 -0
  33. {meerschaum-2.7.1.dist-info → meerschaum-2.7.3.dist-info}/zip-safe +0 -0
meerschaum/utils/sql.py CHANGED
@@ -45,12 +45,12 @@ DROP_IF_EXISTS_FLAVORS = {
45
45
  }
46
46
  SKIP_AUTO_INCREMENT_FLAVORS = {'citus', 'duckdb'}
47
47
  COALESCE_UNIQUE_INDEX_FLAVORS = {'timescaledb', 'postgresql', 'citus'}
48
- update_queries = {
48
+ UPDATE_QUERIES = {
49
49
  'default': """
50
50
  UPDATE {target_table_name} AS f
51
51
  {sets_subquery_none}
52
52
  FROM {target_table_name} AS t
53
- INNER JOIN (SELECT DISTINCT {patch_cols_str} FROM {patch_table_name}) AS p
53
+ INNER JOIN (SELECT {patch_cols_str} FROM {patch_table_name}) AS p
54
54
  ON
55
55
  {and_subquery_t}
56
56
  WHERE
@@ -84,7 +84,7 @@ update_queries = {
84
84
  """,
85
85
  'mysql': """
86
86
  UPDATE {target_table_name} AS f
87
- JOIN (SELECT DISTINCT {patch_cols_str} FROM {patch_table_name}) AS p
87
+ JOIN (SELECT {patch_cols_str} FROM {patch_table_name}) AS p
88
88
  ON
89
89
  {and_subquery_f}
90
90
  {sets_subquery_f}
@@ -100,7 +100,7 @@ update_queries = {
100
100
  """,
101
101
  'mariadb': """
102
102
  UPDATE {target_table_name} AS f
103
- JOIN (SELECT DISTINCT {patch_cols_str} FROM {patch_table_name}) AS p
103
+ JOIN (SELECT {patch_cols_str} FROM {patch_table_name}) AS p
104
104
  ON
105
105
  {and_subquery_f}
106
106
  {sets_subquery_f}
@@ -179,13 +179,13 @@ update_queries = {
179
179
  WHERE ROWID IN (
180
180
  SELECT t.ROWID
181
181
  FROM {target_table_name} AS t
182
- INNER JOIN (SELECT DISTINCT * FROM {patch_table_name}) AS p
182
+ INNER JOIN (SELECT * FROM {patch_table_name}) AS p
183
183
  ON {and_subquery_t}
184
184
  );
185
185
  """,
186
186
  """
187
187
  INSERT INTO {target_table_name} AS f
188
- SELECT DISTINCT {patch_cols_str} FROM {patch_table_name} AS p
188
+ SELECT {patch_cols_str} FROM {patch_table_name} AS p
189
189
  """,
190
190
  ],
191
191
  }
@@ -573,21 +573,21 @@ def dateadd_str(
573
573
  Examples
574
574
  --------
575
575
  >>> dateadd_str(
576
- ... flavor = 'mssql',
577
- ... begin = datetime(2022, 1, 1, 0, 0),
578
- ... number = 1,
576
+ ... flavor='mssql',
577
+ ... begin=datetime(2022, 1, 1, 0, 0),
578
+ ... number=1,
579
579
  ... )
580
580
  "DATEADD(day, 1, CAST('2022-01-01 00:00:00' AS DATETIME2))"
581
581
  >>> dateadd_str(
582
- ... flavor = 'postgresql',
583
- ... begin = datetime(2022, 1, 1, 0, 0),
584
- ... number = 1,
582
+ ... flavor='postgresql',
583
+ ... begin=datetime(2022, 1, 1, 0, 0),
584
+ ... number=1,
585
585
  ... )
586
586
  "CAST('2022-01-01 00:00:00' AS TIMESTAMP) + INTERVAL '1 day'"
587
587
 
588
588
  """
589
589
  from meerschaum.utils.packages import attempt_import
590
- from meerschaum.utils.dtypes.sql import get_db_type_from_pd_type
590
+ from meerschaum.utils.dtypes.sql import get_db_type_from_pd_type, get_pd_type_from_db_type
591
591
  dateutil_parser = attempt_import('dateutil.parser')
592
592
  if 'int' in str(type(begin)).lower():
593
593
  return str(begin)
@@ -619,7 +619,14 @@ def dateadd_str(
619
619
  else f"'{begin}'"
620
620
  )
621
621
 
622
- dt_is_utc = begin_time.tzinfo is not None if begin_time is not None else '+' in str(begin)
622
+ dt_is_utc = (
623
+ begin_time.tzinfo is not None
624
+ if begin_time is not None
625
+ else ('+' in str(begin) or '-' in str(begin).split(':', maxsplit=1)[-1])
626
+ )
627
+ if db_type:
628
+ db_type_is_utc = 'utc' in get_pd_type_from_db_type(db_type).lower()
629
+ dt_is_utc = dt_is_utc or db_type_is_utc
623
630
  db_type = db_type or get_db_type_from_pd_type(
624
631
  ('datetime64[ns, UTC]' if dt_is_utc else 'datetime64[ns]'),
625
632
  flavor=flavor,
@@ -629,22 +636,32 @@ def dateadd_str(
629
636
  if flavor in ('postgresql', 'timescaledb', 'cockroachdb', 'citus'):
630
637
  begin = (
631
638
  f"CAST({begin} AS {db_type})" if begin != 'now'
632
- else "CAST(NOW() AT TIME ZONE 'utc' AS {db_type})"
639
+ else f"CAST(NOW() AT TIME ZONE 'utc' AS {db_type})"
633
640
  )
641
+ if dt_is_utc:
642
+ begin += " AT TIME ZONE 'UTC'"
634
643
  da = begin + (f" + INTERVAL '{number} {datepart}'" if number != 0 else '')
635
644
 
636
645
  elif flavor == 'duckdb':
637
646
  begin = f"CAST({begin} AS {db_type})" if begin != 'now' else 'NOW()'
647
+ if dt_is_utc:
648
+ begin += " AT TIME ZONE 'UTC'"
638
649
  da = begin + (f" + INTERVAL '{number} {datepart}'" if number != 0 else '')
639
650
 
640
651
  elif flavor in ('mssql',):
641
652
  if begin_time and begin_time.microsecond != 0 and not dt_is_utc:
642
653
  begin = begin[:-4] + "'"
643
654
  begin = f"CAST({begin} AS {db_type})" if begin != 'now' else 'GETUTCDATE()'
655
+ if dt_is_utc:
656
+ begin += " AT TIME ZONE 'UTC'"
644
657
  da = f"DATEADD({datepart}, {number}, {begin})" if number != 0 else begin
645
658
 
646
659
  elif flavor in ('mysql', 'mariadb'):
647
- begin = f"CAST({begin} AS DATETIME(6))" if begin != 'now' else 'UTC_TIMESTAMP(6)'
660
+ begin = (
661
+ f"CAST({begin} AS DATETIME(6))"
662
+ if begin != 'now'
663
+ else 'UTC_TIMESTAMP(6)'
664
+ )
648
665
  da = (f"DATE_ADD({begin}, INTERVAL {number} {datepart})" if number != 0 else begin)
649
666
 
650
667
  elif flavor == 'sqlite':
@@ -653,10 +670,10 @@ def dateadd_str(
653
670
  elif flavor == 'oracle':
654
671
  if begin == 'now':
655
672
  begin = str(
656
- datetime.now(timezone.utc).replace(tzinfo=None).strftime('%Y:%m:%d %M:%S.%f')
673
+ datetime.now(timezone.utc).replace(tzinfo=None).strftime(r'%Y:%m:%d %M:%S.%f')
657
674
  )
658
675
  elif begin_time:
659
- begin = str(begin_time.strftime('%Y-%m-%d %H:%M:%S.%f'))
676
+ begin = str(begin_time.strftime(r'%Y-%m-%d %H:%M:%S.%f'))
660
677
  dt_format = 'YYYY-MM-DD HH24:MI:SS.FF'
661
678
  _begin = f"'{begin}'" if begin_time else begin
662
679
  da = (
@@ -691,7 +708,7 @@ def test_connection(
691
708
  warnings.filterwarnings('ignore', 'Could not')
692
709
  try:
693
710
  return retry_connect(**_default_kw)
694
- except Exception as e:
711
+ except Exception:
695
712
  return False
696
713
 
697
714
 
@@ -784,7 +801,7 @@ def sql_item_name(item: str, flavor: str, schema: Optional[str] = None) -> str:
784
801
  ### NOTE: System-reserved words must be quoted.
785
802
  if truncated_item.lower() in (
786
803
  'float', 'varchar', 'nvarchar', 'clob',
787
- 'boolean', 'integer', 'table',
804
+ 'boolean', 'integer', 'table', 'row',
788
805
  ):
789
806
  wrappers = ('"', '"')
790
807
  else:
@@ -1494,9 +1511,9 @@ def get_update_queries(
1494
1511
  ):
1495
1512
  flavor = 'sqlite_delete_insert'
1496
1513
  flavor_key = (f'{flavor}-upsert' if upsert else flavor)
1497
- base_queries = update_queries.get(
1514
+ base_queries = UPDATE_QUERIES.get(
1498
1515
  flavor_key,
1499
- update_queries['default']
1516
+ UPDATE_QUERIES['default']
1500
1517
  )
1501
1518
  if not isinstance(base_queries, list):
1502
1519
  base_queries = [base_queries]
@@ -1577,6 +1594,12 @@ def get_update_queries(
1577
1594
  if not value_cols:
1578
1595
  return ''
1579
1596
 
1597
+ utc_value_cols = {
1598
+ c_name
1599
+ for c_name, c_type in value_cols
1600
+ if ('utc' in get_pd_type_from_db_type(c_type).lower())
1601
+ } if flavor not in TIMEZONE_NAIVE_FLAVORS else set()
1602
+
1580
1603
  cast_func_cols = {
1581
1604
  c_name: (
1582
1605
  ('', '', '')
@@ -1585,7 +1608,11 @@ def get_update_queries(
1585
1608
  and are_dtypes_equal(get_pd_type_from_db_type(c_type), 'bytes')
1586
1609
  )
1587
1610
  else (
1588
- ('CAST(', f" AS {c_type.replace('_', ' ')}", ')')
1611
+ ('CAST(', f" AS {c_type.replace('_', ' ')}", ')' + (
1612
+ " AT TIME ZONE 'UTC'"
1613
+ if c_name in utc_value_cols
1614
+ else ''
1615
+ ))
1589
1616
  if flavor != 'sqlite'
1590
1617
  else ('', '', '')
1591
1618
  )
@@ -1865,6 +1892,7 @@ def get_create_table_queries(
1865
1892
  flavor: str,
1866
1893
  schema: Optional[str] = None,
1867
1894
  primary_key: Optional[str] = None,
1895
+ primary_key_db_type: Optional[str] = None,
1868
1896
  autoincrement: bool = False,
1869
1897
  datetime_column: Optional[str] = None,
1870
1898
  ) -> List[str]:
@@ -1889,6 +1917,9 @@ def get_create_table_queries(
1889
1917
  primary_key: Optional[str], default None
1890
1918
  If provided, designate this column as the primary key in the new table.
1891
1919
 
1920
+ primary_key_db_type: Optional[str], default None
1921
+ If provided, alter the primary key to this type (to set NOT NULL constraint).
1922
+
1892
1923
  autoincrement: bool, default False
1893
1924
  If `True` and `primary_key` is provided, create the `primary_key` column
1894
1925
  as an auto-incrementing integer column.
@@ -1915,6 +1946,7 @@ def get_create_table_queries(
1915
1946
  flavor,
1916
1947
  schema=schema,
1917
1948
  primary_key=primary_key,
1949
+ primary_key_db_type=primary_key_db_type,
1918
1950
  autoincrement=(autoincrement and flavor not in SKIP_AUTO_INCREMENT_FLAVORS),
1919
1951
  datetime_column=datetime_column,
1920
1952
  )
@@ -1926,6 +1958,7 @@ def _get_create_table_query_from_dtypes(
1926
1958
  flavor: str,
1927
1959
  schema: Optional[str] = None,
1928
1960
  primary_key: Optional[str] = None,
1961
+ primary_key_db_type: Optional[str] = None,
1929
1962
  autoincrement: bool = False,
1930
1963
  datetime_column: Optional[str] = None,
1931
1964
  ) -> List[str]:
@@ -2015,6 +2048,7 @@ def _get_create_table_query_from_cte(
2015
2048
  flavor: str,
2016
2049
  schema: Optional[str] = None,
2017
2050
  primary_key: Optional[str] = None,
2051
+ primary_key_db_type: Optional[str] = None,
2018
2052
  autoincrement: bool = False,
2019
2053
  datetime_column: Optional[str] = None,
2020
2054
  ) -> List[str]:
@@ -2035,7 +2069,11 @@ def _get_create_table_query_from_cte(
2035
2069
  if primary_key
2036
2070
  else None
2037
2071
  )
2038
- primary_key_clustered = "CLUSTERED" if not datetime_column else "NONCLUSTERED"
2072
+ primary_key_clustered = (
2073
+ "CLUSTERED"
2074
+ if not datetime_column or datetime_column == primary_key
2075
+ else "NONCLUSTERED"
2076
+ )
2039
2077
  datetime_column_name = (
2040
2078
  sql_item_name(datetime_column, flavor)
2041
2079
  if datetime_column
@@ -2045,80 +2083,106 @@ def _get_create_table_query_from_cte(
2045
2083
  query = query.lstrip()
2046
2084
  if query.lower().startswith('with '):
2047
2085
  final_select_ix = query.lower().rfind('select')
2048
- create_table_query = (
2049
- query[:final_select_ix].rstrip() + ',\n'
2050
- + f"{create_cte_name} AS (\n"
2051
- + query[final_select_ix:]
2052
- + "\n)\n"
2053
- + f"SELECT *\nINTO {new_table_name}\nFROM {create_cte_name}"
2054
- )
2086
+ create_table_queries = [
2087
+ (
2088
+ query[:final_select_ix].rstrip() + ',\n'
2089
+ + f"{create_cte_name} AS (\n"
2090
+ + textwrap.indent(query[final_select_ix:], ' ')
2091
+ + "\n)\n"
2092
+ + f"SELECT *\nINTO {new_table_name}\nFROM {create_cte_name}"
2093
+ ),
2094
+ ]
2055
2095
  else:
2056
- create_table_query = f"""
2057
- SELECT *
2058
- INTO {new_table_name}
2059
- FROM ({query}) AS {create_cte_name}
2060
- """
2061
-
2062
- alter_type_query = f"""
2063
- ALTER TABLE {new_table_name}
2064
- ADD CONSTRAINT {primary_key_constraint_name} PRIMARY KEY {primary_key_clustered} ({primary_key_name})
2065
- """
2096
+ create_table_queries = [
2097
+ (
2098
+ "SELECT *\n"
2099
+ f"INTO {new_table_name}\n"
2100
+ f"FROM (\n{textwrap.indent(query, ' ')}\n) AS {create_cte_name}"
2101
+ ),
2102
+ ]
2103
+
2104
+ alter_type_queries = []
2105
+ if primary_key_db_type:
2106
+ alter_type_queries.extend([
2107
+ (
2108
+ f"ALTER TABLE {new_table_name}\n"
2109
+ f"ALTER COLUMN {primary_key_name} {primary_key_db_type} NOT NULL"
2110
+ ),
2111
+ ])
2112
+ alter_type_queries.extend([
2113
+ (
2114
+ f"ALTER TABLE {new_table_name}\n"
2115
+ f"ADD CONSTRAINT {primary_key_constraint_name} "
2116
+ f"PRIMARY KEY {primary_key_clustered} ({primary_key_name})"
2117
+ ),
2118
+ ])
2066
2119
  elif flavor in (None,):
2067
- create_table_query = f"""
2068
- WITH {create_cte_name} AS ({query})
2069
- CREATE TABLE {new_table_name} AS
2070
- SELECT *
2071
- FROM {create_cte_name}
2072
- """
2120
+ create_table_queries = [
2121
+ (
2122
+ f"WITH {create_cte_name} AS (\n{textwrap.index(query, ' ')}\n)\n"
2123
+ f"CREATE TABLE {new_table_name} AS\n"
2124
+ "SELECT *\n"
2125
+ f"FROM {create_cte_name}"
2126
+ ),
2127
+ ]
2073
2128
 
2074
- alter_type_query = f"""
2075
- ALTER TABLE {new_table_name}
2076
- ADD PRIMARY KEY ({primary_key_name})
2077
- """
2129
+ alter_type_queries = [
2130
+ (
2131
+ f"ALTER TABLE {new_table_name}\n"
2132
+ f"ADD PRIMARY KEY ({primary_key_name})"
2133
+ ),
2134
+ ]
2078
2135
  elif flavor in ('sqlite', 'mysql', 'mariadb', 'duckdb', 'oracle'):
2079
- create_table_query = f"""
2080
- CREATE TABLE {new_table_name} AS
2081
- SELECT *
2082
- FROM ({query})""" + (f""" AS {create_cte_name}""" if flavor != 'oracle' else '') + """
2083
- """
2136
+ create_table_queries = [
2137
+ (
2138
+ f"CREATE TABLE {new_table_name} AS\n"
2139
+ "SELECT *\n"
2140
+ f"FROM (\n{textwrap.indent(query, ' ')}\n)"
2141
+ + (f" AS {create_cte_name}" if flavor != 'oracle' else '')
2142
+ ),
2143
+ ]
2084
2144
 
2085
- alter_type_query = f"""
2086
- ALTER TABLE {new_table_name}
2087
- ADD PRIMARY KEY ({primary_key_name})
2088
- """
2145
+ alter_type_queries = [
2146
+ (
2147
+ f"ALTER TABLE {new_table_name}\n"
2148
+ "ADD PRIMARY KEY ({primary_key_name})"
2149
+ ),
2150
+ ]
2089
2151
  elif flavor == 'timescaledb' and datetime_column and datetime_column != primary_key:
2090
- create_table_query = f"""
2091
- SELECT *
2092
- INTO {new_table_name}
2093
- FROM ({query}) AS {create_cte_name}
2094
- """
2152
+ create_table_queries = [
2153
+ (
2154
+ "SELECT *\n"
2155
+ f"INTO {new_table_name}\n"
2156
+ f"FROM (\n{textwrap.indent(query, ' ')}\n) AS {create_cte_name}\n"
2157
+ ),
2158
+ ]
2095
2159
 
2096
- alter_type_query = f"""
2097
- ALTER TABLE {new_table_name}
2098
- ADD PRIMARY KEY ({datetime_column_name}, {primary_key_name})
2099
- """
2160
+ alter_type_queries = [
2161
+ (
2162
+ f"ALTER TABLE {new_table_name}\n"
2163
+ f"ADD PRIMARY KEY ({datetime_column_name}, {primary_key_name})"
2164
+ ),
2165
+ ]
2100
2166
  else:
2101
- create_table_query = f"""
2102
- SELECT *
2103
- INTO {new_table_name}
2104
- FROM ({query}) AS {create_cte_name}
2105
- """
2167
+ create_table_queries = [
2168
+ (
2169
+ "SELECT *\n"
2170
+ f"INTO {new_table_name}\n"
2171
+ f"FROM (\n{textwrap.indent(query, ' ')}\n) AS {create_cte_name}"
2172
+ ),
2173
+ ]
2106
2174
 
2107
- alter_type_query = f"""
2108
- ALTER TABLE {new_table_name}
2109
- ADD PRIMARY KEY ({primary_key_name})
2110
- """
2175
+ alter_type_queries = [
2176
+ (
2177
+ f"ALTER TABLE {new_table_name}\n"
2178
+ f"ADD PRIMARY KEY ({primary_key_name})"
2179
+ ),
2180
+ ]
2111
2181
 
2112
- create_table_query = textwrap.dedent(create_table_query).lstrip().rstrip()
2113
2182
  if not primary_key:
2114
- return [create_table_query]
2115
-
2116
- alter_type_query = textwrap.dedent(alter_type_query).lstrip().rstrip()
2183
+ return create_table_queries
2117
2184
 
2118
- return [
2119
- create_table_query,
2120
- alter_type_query,
2121
- ]
2185
+ return create_table_queries + alter_type_queries
2122
2186
 
2123
2187
 
2124
2188
  def wrap_query_with_cte(
@@ -2167,6 +2231,7 @@ def wrap_query_with_cte(
2167
2231
  ```
2168
2232
 
2169
2233
  """
2234
+ import textwrap
2170
2235
  sub_query = sub_query.lstrip()
2171
2236
  cte_name_quoted = sql_item_name(cte_name, flavor, None)
2172
2237
 
@@ -2190,7 +2255,7 @@ def wrap_query_with_cte(
2190
2255
 
2191
2256
  return (
2192
2257
  f"WITH {cte_name_quoted} AS (\n"
2193
- f" {sub_query}\n"
2258
+ f"{textwrap.indent(sub_query, ' ')}\n"
2194
2259
  f")\n{parent_query}"
2195
2260
  )
2196
2261
 
@@ -2269,6 +2334,8 @@ def session_execute(
2269
2334
  queries = [queries]
2270
2335
  successes, msgs, results = [], [], []
2271
2336
  for query in queries:
2337
+ if debug:
2338
+ dprint(query)
2272
2339
  query_text = sqlalchemy.text(query)
2273
2340
  fail_msg = "Failed to execute queries."
2274
2341
  try:
@@ -2283,6 +2350,8 @@ def session_execute(
2283
2350
  msgs.append(query_msg)
2284
2351
  results.append(result)
2285
2352
  if not query_success:
2353
+ if debug:
2354
+ dprint("Rolling back session.")
2286
2355
  session.rollback()
2287
2356
  break
2288
2357
  success, msg = all(successes), '\n'.join(msgs)
@@ -67,7 +67,6 @@ def activate_venv(
67
67
  return True
68
68
  import sys
69
69
  import os
70
- from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
71
70
  if venv is not None:
72
71
  init_venv(venv=venv, debug=debug)
73
72
  with LOCKS['active_venvs']:
@@ -379,22 +378,47 @@ def init_venv(
379
378
  import os
380
379
  import pathlib
381
380
  import shutil
381
+ import time
382
382
 
383
383
  from meerschaum.config.static import STATIC_CONFIG
384
- from meerschaum.config._paths import VIRTENV_RESOURCES_PATH, VENVS_CACHE_RESOURCES_PATH
384
+ from meerschaum.config._paths import (
385
+ VIRTENV_RESOURCES_PATH,
386
+ VENVS_CACHE_RESOURCES_PATH,
387
+ )
385
388
  from meerschaum.utils.packages import is_uv_enabled
386
389
 
387
390
  venv_path = VIRTENV_RESOURCES_PATH / venv
388
391
  vtp = venv_target_path(venv=venv, allow_nonexistent=True, debug=debug)
389
392
  docker_home_venv_path = pathlib.Path('/home/meerschaum/venvs/mrsm')
390
-
393
+ lock_path = VENVS_CACHE_RESOURCES_PATH / (venv + '.lock')
391
394
  work_dir_env_var = STATIC_CONFIG['environment']['work_dir']
395
+
396
+ def update_lock(active: bool):
397
+ try:
398
+ if active:
399
+ lock_path.unlink()
400
+ else:
401
+ lock_path.touch()
402
+ except Exception:
403
+ pass
404
+
405
+ def wait_for_lock():
406
+ max_lock_seconds = 1.0
407
+ step_sleep_seconds = 0.1
408
+ init_venv_check_start = time.perf_counter()
409
+ while (time.perf_counter() - init_venv_check_start < max_lock_seconds):
410
+ if not lock_path.exists():
411
+ continue
412
+ time.sleep(step_sleep_seconds)
413
+ update_lock(False)
414
+
392
415
  if (
393
416
  not force
394
417
  and venv == 'mrsm'
395
418
  and os.environ.get(work_dir_env_var, None) is not None
396
419
  and docker_home_venv_path.exists()
397
420
  ):
421
+ wait_for_lock()
398
422
  shutil.move(docker_home_venv_path, venv_path)
399
423
  if verify:
400
424
  verify_venv(venv, debug=debug)
@@ -422,6 +446,9 @@ def init_venv(
422
446
  except FileExistsError:
423
447
  pass
424
448
 
449
+ wait_for_lock()
450
+ update_lock(True)
451
+
425
452
  if uv is not None:
426
453
  _venv_success = run_python_package(
427
454
  'uv',
@@ -437,7 +464,9 @@ def init_venv(
437
464
  _venv_success = run_python_package(
438
465
  'venv',
439
466
  [venv_path.as_posix()] + (
440
- ['--symlinks'] if platform.system() != 'Windows' else []
467
+ ['--symlinks']
468
+ if platform.system() != 'Windows'
469
+ else []
441
470
  ),
442
471
  venv=None, debug=debug
443
472
  ) == 0
@@ -447,8 +476,14 @@ def init_venv(
447
476
  _venv = None
448
477
  if not _venv_success:
449
478
  virtualenv = attempt_import(
450
- 'virtualenv', venv=None, lazy=False, install=(not tried_virtualenv), warn=False,
451
- check_update=False, color=False, debug=debug,
479
+ 'virtualenv',
480
+ venv=None,
481
+ lazy=False,
482
+ install=(not tried_virtualenv),
483
+ warn=False,
484
+ check_update=False,
485
+ color=False,
486
+ debug=debug,
452
487
  )
453
488
  if virtualenv is None:
454
489
  print(
@@ -457,6 +492,7 @@ def init_venv(
457
492
  )
458
493
  if rename_vtp and temp_vtp.exists():
459
494
  temp_vtp.rename(vtp)
495
+ update_lock(False)
460
496
  return False
461
497
 
462
498
  tried_virtualenv = True
@@ -495,6 +531,7 @@ def init_venv(
495
531
  traceback.print_exc()
496
532
  if rename_vtp and temp_vtp.exists():
497
533
  temp_vtp.rename(vtp)
534
+ update_lock(False)
498
535
  return False
499
536
  if verify:
500
537
  verify_venv(venv, debug=debug)
@@ -503,6 +540,7 @@ def init_venv(
503
540
  if rename_vtp and temp_vtp.exists():
504
541
  temp_vtp.rename(vtp)
505
542
 
543
+ update_lock(False)
506
544
  return True
507
545
 
508
546
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: meerschaum
3
- Version: 2.7.1
3
+ Version: 2.7.3
4
4
  Summary: Sync Time-Series Pipes with Meerschaum
5
5
  Home-page: https://meerschaum.io
6
6
  Author: Bennett Meares