meerschaum 2.7.2__py3-none-any.whl → 2.7.4__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.
Files changed (32) 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/start.py +2 -2
  6. meerschaum/actions/verify.py +18 -21
  7. meerschaum/config/_version.py +1 -1
  8. meerschaum/connectors/sql/_fetch.py +45 -26
  9. meerschaum/connectors/sql/_instance.py +4 -4
  10. meerschaum/connectors/sql/_pipes.py +135 -103
  11. meerschaum/core/Pipe/_attributes.py +1 -1
  12. meerschaum/core/Pipe/_dtypes.py +9 -9
  13. meerschaum/core/Pipe/_fetch.py +2 -3
  14. meerschaum/core/Pipe/_sync.py +11 -3
  15. meerschaum/core/Pipe/_verify.py +9 -5
  16. meerschaum/jobs/__init__.py +1 -3
  17. meerschaum/utils/daemon/Daemon.py +1 -1
  18. meerschaum/utils/daemon/StdinFile.py +4 -1
  19. meerschaum/utils/dataframe.py +10 -2
  20. meerschaum/utils/dtypes/sql.py +1 -1
  21. meerschaum/utils/formatting/__init__.py +5 -25
  22. meerschaum/utils/formatting/_pipes.py +9 -6
  23. meerschaum/utils/sql.py +156 -87
  24. meerschaum/utils/venv/__init__.py +61 -13
  25. {meerschaum-2.7.2.dist-info → meerschaum-2.7.4.dist-info}/METADATA +1 -1
  26. {meerschaum-2.7.2.dist-info → meerschaum-2.7.4.dist-info}/RECORD +32 -32
  27. {meerschaum-2.7.2.dist-info → meerschaum-2.7.4.dist-info}/LICENSE +0 -0
  28. {meerschaum-2.7.2.dist-info → meerschaum-2.7.4.dist-info}/NOTICE +0 -0
  29. {meerschaum-2.7.2.dist-info → meerschaum-2.7.4.dist-info}/WHEEL +0 -0
  30. {meerschaum-2.7.2.dist-info → meerschaum-2.7.4.dist-info}/entry_points.txt +0 -0
  31. {meerschaum-2.7.2.dist-info → meerschaum-2.7.4.dist-info}/top_level.txt +0 -0
  32. {meerschaum-2.7.2.dist-info → meerschaum-2.7.4.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,54 @@ 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 not active:
399
+ if debug:
400
+ print(f"Releasing lock: '{lock_path}'")
401
+ lock_path.unlink()
402
+ else:
403
+ if debug:
404
+ print(f"Acquiring lock: '{lock_path}'")
405
+ lock_path.touch()
406
+ except Exception:
407
+ pass
408
+
409
+ def wait_for_lock():
410
+ max_lock_seconds = 1.0
411
+ step_sleep_seconds = 0.1
412
+ init_venv_check_start = time.perf_counter()
413
+ while (time.perf_counter() - init_venv_check_start < max_lock_seconds):
414
+ if not lock_path.exists():
415
+ break
416
+
417
+ if debug:
418
+ print(f"Lock exists for '{venv}', sleeping...")
419
+ time.sleep(step_sleep_seconds)
420
+ update_lock(False)
421
+
392
422
  if (
393
423
  not force
394
424
  and venv == 'mrsm'
395
425
  and os.environ.get(work_dir_env_var, None) is not None
396
426
  and docker_home_venv_path.exists()
397
427
  ):
428
+ wait_for_lock()
398
429
  shutil.move(docker_home_venv_path, venv_path)
399
430
  if verify:
400
431
  verify_venv(venv, debug=debug)
@@ -417,10 +448,12 @@ def init_venv(
417
448
  rename_vtp = vtp.exists() and not temp_vtp.exists()
418
449
 
419
450
  if rename_vtp:
420
- try:
421
- vtp.rename(temp_vtp)
422
- except FileExistsError:
423
- pass
451
+ if debug:
452
+ print(f"Moving '{vtp}' to '{temp_vtp}'...")
453
+ shutil.move(vtp, temp_vtp)
454
+
455
+ wait_for_lock()
456
+ update_lock(True)
424
457
 
425
458
  if uv is not None:
426
459
  _venv_success = run_python_package(
@@ -437,7 +470,9 @@ def init_venv(
437
470
  _venv_success = run_python_package(
438
471
  'venv',
439
472
  [venv_path.as_posix()] + (
440
- ['--symlinks'] if platform.system() != 'Windows' else []
473
+ ['--symlinks']
474
+ if platform.system() != 'Windows'
475
+ else []
441
476
  ),
442
477
  venv=None, debug=debug
443
478
  ) == 0
@@ -447,8 +482,14 @@ def init_venv(
447
482
  _venv = None
448
483
  if not _venv_success:
449
484
  virtualenv = attempt_import(
450
- 'virtualenv', venv=None, lazy=False, install=(not tried_virtualenv), warn=False,
451
- check_update=False, color=False, debug=debug,
485
+ 'virtualenv',
486
+ venv=None,
487
+ lazy=False,
488
+ install=(not tried_virtualenv),
489
+ warn=False,
490
+ check_update=False,
491
+ color=False,
492
+ debug=debug,
452
493
  )
453
494
  if virtualenv is None:
454
495
  print(
@@ -456,7 +497,10 @@ def init_venv(
456
497
  + "Please install `virtualenv` via pip then restart Meerschaum."
457
498
  )
458
499
  if rename_vtp and temp_vtp.exists():
459
- temp_vtp.rename(vtp)
500
+ if debug:
501
+ print(f"Moving '{temp_vtp}' back to '{vtp}'...")
502
+ shutil.move(temp_vtp, vtp)
503
+ update_lock(False)
460
504
  return False
461
505
 
462
506
  tried_virtualenv = True
@@ -494,15 +538,19 @@ def init_venv(
494
538
  import traceback
495
539
  traceback.print_exc()
496
540
  if rename_vtp and temp_vtp.exists():
497
- temp_vtp.rename(vtp)
541
+ shutil.move(temp_vtp, vtp)
542
+ update_lock(False)
498
543
  return False
499
544
  if verify:
500
545
  verify_venv(venv, debug=debug)
501
546
  verified_venvs.add(venv)
502
547
 
503
548
  if rename_vtp and temp_vtp.exists():
504
- temp_vtp.rename(vtp)
549
+ if debug:
550
+ print(f"Cleanup: move '{temp_vtp}' back to '{vtp}'.")
551
+ shutil.move(temp_vtp, vtp)
505
552
 
553
+ update_lock(False)
506
554
  return True
507
555
 
508
556
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: meerschaum
3
- Version: 2.7.2
3
+ Version: 2.7.4
4
4
  Summary: Sync Time-Series Pipes with Meerschaum
5
5
  Home-page: https://meerschaum.io
6
6
  Author: Bennett Meares