primitive 0.2.46__tar.gz → 0.2.48__tar.gz

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 (113) hide show
  1. {primitive-0.2.46 → primitive-0.2.48}/PKG-INFO +1 -1
  2. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/__about__.py +1 -1
  3. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/agent/runner.py +29 -119
  4. {primitive-0.2.46 → primitive-0.2.48}/.git-hooks/pre-commit +0 -0
  5. {primitive-0.2.46 → primitive-0.2.48}/.gitattributes +0 -0
  6. {primitive-0.2.46 → primitive-0.2.48}/.github/workflows/lint.yml +0 -0
  7. {primitive-0.2.46 → primitive-0.2.48}/.github/workflows/publish.yml +0 -0
  8. {primitive-0.2.46 → primitive-0.2.48}/.github/workflows/pyright.yml +0 -0
  9. {primitive-0.2.46 → primitive-0.2.48}/.gitignore +0 -0
  10. {primitive-0.2.46 → primitive-0.2.48}/.vscode/extensions.json +0 -0
  11. {primitive-0.2.46 → primitive-0.2.48}/.vscode/settings.json +0 -0
  12. {primitive-0.2.46 → primitive-0.2.48}/LICENSE.txt +0 -0
  13. {primitive-0.2.46 → primitive-0.2.48}/Makefile +0 -0
  14. {primitive-0.2.46 → primitive-0.2.48}/README.md +0 -0
  15. {primitive-0.2.46 → primitive-0.2.48}/linux setup.md +0 -0
  16. {primitive-0.2.46 → primitive-0.2.48}/pyproject.toml +0 -0
  17. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/__init__.py +0 -0
  18. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/agent/__init__.py +0 -0
  19. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/agent/actions.py +0 -0
  20. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/agent/commands.py +0 -0
  21. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/agent/uploader.py +0 -0
  22. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/auth/__init__.py +0 -0
  23. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/auth/actions.py +0 -0
  24. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/auth/commands.py +0 -0
  25. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/auth/graphql/__init__.py +0 -0
  26. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/auth/graphql/queries.py +0 -0
  27. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/cli.py +0 -0
  28. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/client.py +0 -0
  29. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/daemons/__init__.py +0 -0
  30. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/daemons/actions.py +0 -0
  31. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/daemons/commands.py +0 -0
  32. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/daemons/launch_agents.py +0 -0
  33. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/daemons/launch_service.py +0 -0
  34. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/daemons/ui.py +0 -0
  35. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/exec/__init__.py +0 -0
  36. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/exec/actions.py +0 -0
  37. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/exec/commands.py +0 -0
  38. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/exec/interactive.py +0 -0
  39. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/files/__init__.py +0 -0
  40. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/files/actions.py +0 -0
  41. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/files/commands.py +0 -0
  42. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/files/graphql/__init__.py +0 -0
  43. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/files/graphql/fragments.py +0 -0
  44. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/files/graphql/mutations.py +0 -0
  45. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/files/graphql/queries.py +0 -0
  46. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/git/__init__.py +0 -0
  47. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/git/actions.py +0 -0
  48. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/git/commands.py +0 -0
  49. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/git/graphql/__init__.py +0 -0
  50. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/git/graphql/queries.py +0 -0
  51. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/graphql/__init__.py +0 -0
  52. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/graphql/relay.py +0 -0
  53. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/graphql/sdk.py +0 -0
  54. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/graphql/utility_fragments.py +0 -0
  55. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/hardware/__init__.py +0 -0
  56. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/hardware/actions.py +0 -0
  57. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/hardware/android.py +0 -0
  58. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/hardware/commands.py +0 -0
  59. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/hardware/graphql/__init__.py +0 -0
  60. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/hardware/graphql/fragments.py +0 -0
  61. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/hardware/graphql/mutations.py +0 -0
  62. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/hardware/graphql/queries.py +0 -0
  63. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/hardware/ui.py +0 -0
  64. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/jobs/__init__.py +0 -0
  65. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/jobs/actions.py +0 -0
  66. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/jobs/commands.py +0 -0
  67. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/jobs/graphql/__init__.py +0 -0
  68. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/jobs/graphql/fragments.py +0 -0
  69. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/jobs/graphql/mutations.py +0 -0
  70. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/jobs/graphql/queries.py +0 -0
  71. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/monitor/actions.py +0 -0
  72. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/monitor/commands.py +0 -0
  73. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/organizations/__init__.py +0 -0
  74. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/organizations/actions.py +0 -0
  75. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/organizations/commands.py +0 -0
  76. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/organizations/graphql/__init__.py +0 -0
  77. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/organizations/graphql/fragments.py +0 -0
  78. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/organizations/graphql/mutations.py +0 -0
  79. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/organizations/graphql/queries.py +0 -0
  80. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/projects/__init__.py +0 -0
  81. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/projects/actions.py +0 -0
  82. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/projects/commands.py +0 -0
  83. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/projects/graphql/__init__.py +0 -0
  84. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/projects/graphql/fragments.py +0 -0
  85. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/projects/graphql/mutations.py +0 -0
  86. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/projects/graphql/queries.py +0 -0
  87. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/provisioning/__init__.py +0 -0
  88. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/provisioning/actions.py +0 -0
  89. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/provisioning/graphql/__init__.py +0 -0
  90. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/provisioning/graphql/queries.py +0 -0
  91. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/reservations/__init__.py +0 -0
  92. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/reservations/actions.py +0 -0
  93. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/reservations/commands.py +0 -0
  94. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/reservations/graphql/__init__.py +0 -0
  95. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/reservations/graphql/fragments.py +0 -0
  96. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/reservations/graphql/mutations.py +0 -0
  97. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/reservations/graphql/queries.py +0 -0
  98. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/__init__.py +0 -0
  99. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/actions.py +0 -0
  100. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/auth.py +0 -0
  101. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/cache.py +0 -0
  102. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/chunk_size.py +0 -0
  103. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/config.py +0 -0
  104. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/daemons.py +0 -0
  105. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/exceptions.py +0 -0
  106. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/logging.py +0 -0
  107. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/memory_size.py +0 -0
  108. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/printer.py +0 -0
  109. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/psutil.py +0 -0
  110. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/shell.py +0 -0
  111. {primitive-0.2.46 → primitive-0.2.48}/src/primitive/utils/text.py +0 -0
  112. {primitive-0.2.46 → primitive-0.2.48}/tests/__init__.py +0 -0
  113. {primitive-0.2.46 → primitive-0.2.48}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primitive
3
- Version: 0.2.46
3
+ Version: 0.2.48
4
4
  Project-URL: Documentation, https://github.com//primitivecorp/primitive-cli#readme
5
5
  Project-URL: Issues, https://github.com//primitivecorp/primitive-cli/issues
6
6
  Project-URL: Source, https://github.com//primitivecorp/primitive-cli
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2025-present Dylan Stein <dylan@primitive.tech>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.2.46"
4
+ __version__ = "0.2.48"
@@ -1,6 +1,5 @@
1
1
  import asyncio
2
2
  import os
3
- import re
4
3
  import shutil
5
4
  import typing
6
5
  from enum import Enum, IntEnum
@@ -13,7 +12,6 @@ from loguru import logger
13
12
  from primitive.utils.cache import get_artifacts_cache, get_logs_cache, get_sources_cache
14
13
  from primitive.utils.logging import fmt, log_context
15
14
  from primitive.utils.psutil import kill_process_and_children
16
- from primitive.utils.shell import env_to_dict
17
15
 
18
16
  try:
19
17
  from yaml import CLoader as Loader
@@ -23,13 +21,7 @@ except ImportError:
23
21
  if typing.TYPE_CHECKING:
24
22
  import primitive.client
25
23
 
26
- CHUNK_SIZE = 64 * 1024
27
- ENV_VAR_LOOKUP_START = "_ENV_VAR_LOOKUP_START"
28
- START_DELIMITER_SIZE = len(bytes(ENV_VAR_LOOKUP_START, encoding="utf-8"))
29
- ENV_VAR_LOOKUP_END = "_ENV_VAR_LOOKUP_END"
30
- END_DELIMITER_SIZE = len(bytes(ENV_VAR_LOOKUP_END, encoding="utf-8"))
31
-
32
- assert CHUNK_SIZE > START_DELIMITER_SIZE + END_DELIMITER_SIZE
24
+ BUFFER_SIZE = 4096
33
25
 
34
26
 
35
27
  class Task(TypedDict):
@@ -69,7 +61,6 @@ class Runner:
69
61
  self.job_run = job_run
70
62
  self.job_settings = job_run["jobSettings"]
71
63
  self.config = None
72
- self.source_dir: Path | None = None
73
64
  self.initial_env = {}
74
65
  self.modified_env = {}
75
66
  self.file_logger = None
@@ -140,6 +131,11 @@ class Runner:
140
131
  self.job_run["gitCommit"]["repoFullName"]
141
132
  )
142
133
 
134
+ if "VIRTUAL_ENV" in self.initial_env:
135
+ del self.initial_env["VIRTUAL_ENV"]
136
+ if "SHELL" in self.initial_env:
137
+ self.initial_env["SHELL"] = "/bin/bash"
138
+
143
139
  @log_context(label="execute")
144
140
  def execute_job_run(self) -> None:
145
141
  self.modified_env = {**self.initial_env}
@@ -253,13 +249,11 @@ class Runner:
253
249
  # Adding an additional echo and utilizing stdbuf to force line buffering
254
250
  # This ensures that the environment variables and starting delimiter are
255
251
  # always in a new chunk, vastly simplifying our parsing logic
256
- args = [
257
- "/bin/bash",
258
- "-c",
259
- f"{cmd} && echo -n '{ENV_VAR_LOOKUP_START}' && env && echo -n '{ENV_VAR_LOOKUP_END}'",
260
- ]
252
+ args = ["/bin/bash", "-c", cmd]
261
253
 
262
- logger.info(f"Executing command {i + 1}/{len(commands)}: {cmd}")
254
+ logger.info(
255
+ f"Executing command {i + 1}/{len(commands)}: {cmd} at {self.source_dir / task.get('workdir', '')}"
256
+ )
263
257
 
264
258
  process = await asyncio.create_subprocess_exec(
265
259
  *args,
@@ -281,13 +275,9 @@ class Runner:
281
275
  kill_process_and_children(pid=process.pid)
282
276
  return False
283
277
 
284
- stdout_failed, stderr_failed = await asyncio.gather(
285
- self.log_cmd(
286
- process=process, stream=process.stdout, tags=task.get("tags", {})
287
- ),
288
- self.log_cmd(
289
- process=process, stream=process.stderr, tags=task.get("tags", {})
290
- ),
278
+ await asyncio.gather(
279
+ self.log_cmd(stream=process.stdout, level=LogLevel.INFO),
280
+ self.log_cmd(stream=process.stderr, level=LogLevel.ERROR),
291
281
  )
292
282
 
293
283
  returncode = await process.wait()
@@ -301,106 +291,26 @@ class Runner:
301
291
  f"Task {task['label']} failed on '{cmd}' with return code {returncode}"
302
292
  )
303
293
  return True
304
- elif stdout_failed or stderr_failed:
305
- logger.error(f"Task {task['label']} failed on '{cmd}'")
306
- return True
307
294
 
308
295
  logger.success(f"Completed {task['label']} task")
309
296
  return False
310
297
 
311
- async def log_cmd(self, process, stream, tags: Dict = {}) -> bool:
312
- failure_detected = False
313
- parse_environment = False
314
- last_chunk_buffer = b""
315
- environment_buffer = b""
316
- while chunk := await stream.read(CHUNK_SIZE):
317
- if parse_environment:
318
- environment_buffer += chunk
319
- continue
320
-
321
- # First, look for start delimiter in chunk
322
- full_chunk = last_chunk_buffer + chunk
323
- last_chunk_buffer = b""
324
- start_index = full_chunk.find(bytes(ENV_VAR_LOOKUP_START, encoding="utf-8"))
325
-
326
- if start_index != -1:
327
- environment_buffer = full_chunk[start_index + START_DELIMITER_SIZE :]
328
- processed_lines = await self.read_chunk(full_chunk[:start_index])
329
- parse_environment = True
330
- else:
331
- processed_lines = await self.read_chunk(full_chunk)
332
-
333
- while (
334
- len(last_chunk_buffer) < START_DELIMITER_SIZE
335
- and len(processed_lines) > 0
336
- ):
337
- last_chunk_buffer += bytes(
338
- processed_lines.pop() + "\n", encoding="utf-8"
339
- )
340
-
341
- # Handle logging
342
- parse_logs = self.job_settings["parseLogs"]
343
- parse_stderr = self.job_settings["parseStderr"]
344
-
345
- for line in processed_lines:
346
- level = LogLevel.INFO
347
- tag = None
348
- if (parse_logs and "error" in line.lower()) or (
349
- parse_stderr and stream is process.stderr
350
- ):
351
- level = LogLevel.ERROR
352
- elif parse_logs and "warning" in line.lower():
353
- level = LogLevel.WARNING
354
-
355
- # If we already detected a failure, skip checking
356
- if not failure_detected:
357
- failure_detected = (
358
- level == LogLevel.ERROR
359
- and self.job_settings["failureLevel"] >= FailureLevel.ERROR
360
- ) or (
361
- level == LogLevel.WARNING
362
- and self.job_settings["failureLevel"] >= FailureLevel.WARNING
363
- )
364
-
365
- # Tag on the first matching regex in the list
366
- for tag_key, regex in tags.items():
367
- pattern = re.compile(regex)
368
- if pattern.match(line):
369
- tag = tag_key
370
- break
371
-
372
- logger.bind(tag=tag).log(level.value, line)
373
-
374
- start_index = environment_buffer.find(
375
- bytes(ENV_VAR_LOOKUP_END, encoding="utf-8")
376
- )
377
- if parse_environment and start_index == -1:
378
- logger.error("Environment variable buffer did not contain end delimiter")
379
- failure_detected = True
380
- return failure_detected
381
-
382
- environment_buffer = environment_buffer[:start_index]
383
- new_env_vars = env_to_dict(environment_buffer)
384
- if len(new_env_vars.keys()) > 0:
385
- self.modified_env = {**self.modified_env, **new_env_vars}
386
- return failure_detected
387
-
388
- async def read_chunk(self, chunk: bytes) -> List[str]:
389
- """Converts a chunk of bytes into proper lines."""
390
- lines = []
391
- current_line = ""
392
- for char in chunk.decode():
393
- if char in ("\n", "\r"):
394
- lines.append(current_line)
395
- current_line = ""
396
- else:
397
- current_line += char
398
-
399
- # Handle extra characters in the buffer
400
- if len(current_line) > 0:
401
- lines.append(current_line)
402
-
403
- return [line for line in lines if len(line) > 0]
298
+ async def log_cmd(
299
+ self,
300
+ stream: asyncio.StreamReader | None,
301
+ level: LogLevel,
302
+ ):
303
+ buffer = bytearray()
304
+ while stream and not stream.at_eof():
305
+ chunk = await stream.read(BUFFER_SIZE)
306
+ if not chunk:
307
+ break
308
+ buffer += chunk
309
+ while b"\n" in buffer:
310
+ line, _, buffer = buffer.partition(b"\n")
311
+ logger.log(level.value, line.decode(errors="replace"))
312
+ if buffer:
313
+ logger.log(level.value, buffer.decode(errors="replace"))
404
314
 
405
315
  @log_context(label="cleanup")
406
316
  def cleanup(self) -> None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes