primitive 0.2.35__py3-none-any.whl → 0.2.37__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.
primitive/__about__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present Dylan Stein <dylan@primitive.tech>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.2.35"
4
+ __version__ = "0.2.37"
@@ -1,5 +1,6 @@
1
1
  import sys
2
2
  from time import sleep
3
+ from typing import Optional
3
4
 
4
5
  from loguru import logger
5
6
 
@@ -12,9 +13,7 @@ from primitive.utils.actions import BaseAction
12
13
 
13
14
 
14
15
  class Agent(BaseAction):
15
- def execute(
16
- self,
17
- ):
16
+ def execute(self, job_run_id: Optional[str] = None):
18
17
  logger.remove()
19
18
  logger.add(
20
19
  sink=sys.stderr,
@@ -26,6 +25,12 @@ class Agent(BaseAction):
26
25
  logger.info("primitive agent")
27
26
  logger.info(f"Version: {__version__}")
28
27
 
28
+ # TODO: tighten logic for determining if we're running in a container
29
+ RUNNING_IN_CONTAINER = False
30
+ if job_run_id is not None:
31
+ logger.info("Running in container...")
32
+ RUNNING_IN_CONTAINER = True
33
+
29
34
  # Wait for monitor to make database
30
35
  wait_for_db()
31
36
 
@@ -100,6 +105,10 @@ class Agent(BaseAction):
100
105
  # This should probably eventually be another daemon?
101
106
  uploader.scan()
102
107
 
108
+ if RUNNING_IN_CONTAINER:
109
+ logger.info("Running in container, exiting after job run")
110
+ break
111
+
103
112
  sleep(5)
104
113
  except KeyboardInterrupt:
105
114
  logger.info("Stopping primitive agent...")
@@ -7,8 +7,9 @@ if typing.TYPE_CHECKING:
7
7
 
8
8
 
9
9
  @click.command("agent")
10
+ @click.option("--job-run-id", type=str, help="Explicit Job Run to pull")
10
11
  @click.pass_context
11
- def cli(context):
12
+ def cli(context, job_run_id: typing.Optional[str] = None):
12
13
  """agent"""
13
14
  primitive: Primitive = context.obj.get("PRIMITIVE")
14
- primitive.agent.execute()
15
+ primitive.agent.execute(job_run_id=job_run_id)
primitive/agent/runner.py CHANGED
@@ -3,7 +3,6 @@ import os
3
3
  import re
4
4
  import shutil
5
5
  import typing
6
- from abc import abstractmethod
7
6
  from enum import Enum, IntEnum
8
7
  from pathlib import Path, PurePath
9
8
  from typing import Dict, List, TypedDict
@@ -13,6 +12,7 @@ from loguru import logger
13
12
 
14
13
  from ..db.models import JobRun
15
14
  from ..utils.cache import get_artifacts_cache, get_logs_cache, get_sources_cache
15
+ from ..utils.logging import fmt, log_context
16
16
  from ..utils.shell import env_to_dict
17
17
 
18
18
  try:
@@ -57,16 +57,6 @@ class LogLevel(Enum):
57
57
  WARNING = "WARNING"
58
58
 
59
59
 
60
- # Log Counter
61
- class LogCounter:
62
- count = 0
63
-
64
- @classmethod
65
- def next(cls) -> int:
66
- cls.count += 1
67
- return cls.count
68
-
69
-
70
60
  class Runner:
71
61
  def __init__(
72
62
  self,
@@ -79,7 +69,7 @@ class Runner:
79
69
  self.job_run = job_run
80
70
  self.job_settings = job_run["jobSettings"]
81
71
  self.config = None
82
- self.source_dir: Path = None
72
+ self.source_dir: Path | None = None
83
73
  self.initial_env = {}
84
74
  self.modified_env = {}
85
75
  self.file_logger = None
@@ -91,11 +81,11 @@ class Runner:
91
81
  self.file_logger = logger.add(
92
82
  Path(get_logs_cache(self.job_run["id"]) / log_name),
93
83
  rotation=max_log_size,
94
- format=Runner.fmt,
84
+ format=fmt,
95
85
  backtrace=True,
96
- diagnose=True,
97
86
  )
98
87
 
88
+ @log_context(label="setup")
99
89
  def setup(self) -> None:
100
90
  # Attempt to download the job source code
101
91
  git_repo_full_name = self.job_run["gitCommit"]["repoFullName"]
@@ -155,66 +145,62 @@ class Runner:
155
145
  self.job_run["gitCommit"]["repoFullName"]
156
146
  )
157
147
 
148
+ @log_context(label="execute")
158
149
  def execute(self) -> None:
159
150
  logger.info(f"Executing {self.job['slug']} job")
160
151
  self.primitive.jobs.job_run_update(
161
152
  self.job_run["id"], status="request_in_progress"
162
153
  )
163
- self.modified_env = {**self.initial_env}
164
154
 
155
+ self.modified_env = {**self.initial_env}
165
156
  task_failed = False
166
157
  cancelled = False
167
158
 
168
159
  for task in self.config["executes"]:
169
- # the get status check here is to ensure that if cancel is called
170
- # while one task is running, we do not run any OTHER laebeled tasks
171
- # THIS is required for MULTI STEP JOBS
160
+ # Everything inside this loop should be contextualized with the task label
161
+ # this way we aren't jumping back and forth between the task label and "execute"
162
+ with logger.contextualize(label=task["label"]):
163
+ # the get status check here is to ensure that if cancel is called
164
+ # while one task is running, we do not run any OTHER labeled tasks
165
+ # THIS is required for MULTI STEP JOBS
166
+ status = self.primitive.jobs.get_job_status(self.job_run["id"])
167
+ status_value = status.data["jobRun"]["status"]
168
+ conclusion_value = status.data["jobRun"]["conclusion"]
169
+
170
+ if status_value == "completed" and conclusion_value == "cancelled":
171
+ cancelled = True
172
+ break
173
+
174
+ # Everything within this block should be contextualized as user logs
175
+ with logger.contextualize(type="user"):
176
+ with asyncio.Runner() as async_runner:
177
+ if task_failed := async_runner.run(self.run_task(task)):
178
+ break
179
+
180
+ # FOR NONE MULTI STEP JOBS
181
+ # we still have to check that the job was cancelled here as well
182
+ with logger.contextualize(label="conclusion"):
172
183
  status = self.primitive.jobs.get_job_status(self.job_run["id"])
173
184
  status_value = status.data["jobRun"]["status"]
174
185
  conclusion_value = status.data["jobRun"]["conclusion"]
175
-
176
186
  if status_value == "completed" and conclusion_value == "cancelled":
177
187
  cancelled = True
178
- break
179
188
 
180
- with logger.contextualize(label=task["label"]):
181
- with asyncio.Runner() as async_runner:
182
- if task_failed := async_runner.run(self.run_task(task)):
183
- break
189
+ if cancelled:
190
+ logger.warning("Job cancelled by user")
191
+ return
184
192
 
185
- number_of_files_produced = self.get_number_of_files_produced()
186
- logger.info(
187
- f"Produced {number_of_files_produced} files for {self.job['slug']} job"
188
- )
193
+ conclusion = "success"
194
+ if task_failed:
195
+ conclusion = "failure"
196
+ else:
197
+ logger.success(f"Completed {self.job['slug']} job")
189
198
 
190
- # FOR NONE MULTI STEP JOBS
191
- # we still have to check that the job was cancelled here as well
192
- status = self.primitive.jobs.get_job_status(self.job_run["id"])
193
- status_value = status.data["jobRun"]["status"]
194
- conclusion_value = status.data["jobRun"]["conclusion"]
195
- if status_value == "completed" and conclusion_value == "cancelled":
196
- cancelled = True
197
-
198
- if cancelled:
199
- logger.warning("Job cancelled by user")
200
199
  self.primitive.jobs.job_run_update(
201
200
  self.job_run["id"],
202
- number_of_files_produced=number_of_files_produced,
201
+ status="request_completed",
202
+ conclusion=conclusion,
203
203
  )
204
- return
205
-
206
- conclusion = "success"
207
- if task_failed:
208
- conclusion = "failure"
209
- else:
210
- logger.success(f"Completed {self.job['slug']} job")
211
-
212
- self.primitive.jobs.job_run_update(
213
- self.job_run["id"],
214
- status="request_completed",
215
- conclusion=conclusion,
216
- number_of_files_produced=number_of_files_produced,
217
- )
218
204
 
219
205
  def get_number_of_files_produced(self) -> int:
220
206
  """Returns the number of files produced by the job."""
@@ -315,6 +301,7 @@ class Runner:
315
301
  logger.error(f"Task {task['label']} failed on '{cmd}'")
316
302
  return True
317
303
 
304
+ logger.success(f"Completed {task['label']} task")
318
305
  return False
319
306
 
320
307
  async def log_cmd(self, process, stream, tags: Dict = {}) -> bool:
@@ -409,9 +396,8 @@ class Runner:
409
396
 
410
397
  return [line for line in lines if len(line) > 0]
411
398
 
399
+ @log_context(label="cleanup")
412
400
  def cleanup(self) -> None:
413
- logger.remove(self.file_logger)
414
-
415
401
  if "stores" not in self.config:
416
402
  return
417
403
 
@@ -427,23 +413,13 @@ class Runner:
427
413
 
428
414
  shutil.rmtree(path=self.source_dir)
429
415
 
430
- @abstractmethod
431
- def fmt(record) -> str:
432
- extra = record["extra"]
433
- # Delimiters with empty space MUST exist for LogQL pattern matching
434
- label = extra.get("label", None)
435
- tag = extra.get("tag", None)
436
- context = f"{label} | " if label else " | "
437
- context += f"{tag} | " if tag else " | "
438
-
439
- log = (
440
- f"{LogCounter.next()} | "
441
- "<green>{time:YYYY-MM-DD HH:mm:ss.SSS!UTC}</green> | "
442
- "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
443
- "<level>{level}</level> | "
444
- f"{context}"
445
- "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
446
- "<level>{message}</level>\n"
416
+ number_of_files_produced = self.get_number_of_files_produced()
417
+ logger.info(
418
+ f"Produced {number_of_files_produced} files for {self.job['slug']} job"
419
+ )
420
+ self.primitive.jobs.job_run_update(
421
+ self.job_run["id"],
422
+ number_of_files_produced=number_of_files_produced,
447
423
  )
448
424
 
449
- return log
425
+ logger.remove(self.file_logger)
@@ -0,0 +1,45 @@
1
+ import json
2
+ from datetime import timezone
3
+ from functools import wraps
4
+
5
+ from loguru import logger
6
+
7
+
8
+ def log_context(**context):
9
+ def decorator(func):
10
+ @wraps(func)
11
+ def wrapper(*args, **kwargs):
12
+ with logger.contextualize(**context):
13
+ return func(*args, **kwargs)
14
+
15
+ return wrapper
16
+
17
+ return decorator
18
+
19
+
20
+ def fmt(record) -> str:
21
+ extra = record["extra"]
22
+ label = extra.get("label", None)
23
+ tag = extra.get("tag", None)
24
+ type = extra.get("type", "system")
25
+
26
+ context_object = {
27
+ "label": label,
28
+ "type": type,
29
+ "utc": record["time"]
30
+ .astimezone(timezone.utc)
31
+ .strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
32
+ "level": record["level"].name,
33
+ "name": record["name"],
34
+ "function": record["function"],
35
+ "line": record["line"],
36
+ "message": record["message"],
37
+ }
38
+
39
+ if tag:
40
+ context_object["tag"] = tag
41
+
42
+ # Loguru will fail if you return a string that doesn't select
43
+ # something within its record
44
+ record["extra"]["serialized"] = json.dumps(context_object)
45
+ return "{extra[serialized]}\n"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: primitive
3
- Version: 0.2.35
3
+ Version: 0.2.37
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,11 +1,11 @@
1
- primitive/__about__.py,sha256=hEyqfjOzciSUTNTdMBjYfDhPQ78HUi4rV_w9imBssec,130
1
+ primitive/__about__.py,sha256=k_4SlrPFVlD5xhSWVsBLRpaxZRnyooKrAmVPOvghuL0,130
2
2
  primitive/__init__.py,sha256=bwKdgggKNVssJFVPfKSxqFMz4IxSr54WWbmiZqTMPNI,106
3
3
  primitive/cli.py,sha256=g7EtHI9MATAB0qQu5w-WzbXtxz_8zu8z5E7sETmMkKU,2509
4
4
  primitive/client.py,sha256=h8WZVnQylVe0vbpuyC8YZHl2JyITSPC-1HbUcmrE5pc,3623
5
5
  primitive/agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- primitive/agent/actions.py,sha256=VHlNTw2M-T1MRajpIBu2weT8AsdDdYtlgbBnRBYYeco,3692
7
- primitive/agent/commands.py,sha256=cK7d3OcN5Z65gQWVZFQ-Y9ddw9Pes4f9OVBpeMsj5sE,255
8
- primitive/agent/runner.py,sha256=u1uEJiXUi2ps7wHFYo5iOW4WURo4ObnApUbzhsG-6AU,15784
6
+ primitive/agent/actions.py,sha256=vq4_CZy7Mn37jUg-JPuJlMLtcx4sMXp6tUNvXGZOOyQ,4128
7
+ primitive/agent/commands.py,sha256=kqa-PGqmzS-APd4BSMAkX4l8SdK5N1PRQwq9S8jBjsw,390
8
+ primitive/agent/runner.py,sha256=ZlFC7Eq0NXOSCOvMtDu9gWKF2sa4QMkeG-wH-R-9iOc,15463
9
9
  primitive/agent/uploader.py,sha256=ZzrzsajNBogwEC7mT6Ejy0h2Jd9axMYGzt9pbCvVMlk,3171
10
10
  primitive/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  primitive/auth/actions.py,sha256=9NIEXJ1BNJutJs6AMMSjMN_ziONUAUhY_xHwojYJCLA,942
@@ -92,12 +92,13 @@ primitive/utils/chunk_size.py,sha256=PAuVuirUTA9oRXyjo1c6MWxo31WVBRkWMuWw-AS58Bw
92
92
  primitive/utils/config.py,sha256=DlFM5Nglo22WPtbpZSVtH7NX-PTMaKYlcrUE7GPRG4c,1058
93
93
  primitive/utils/daemons.py,sha256=mSoSHitiGfS4KYAEK9sKsiv_YcACHKgY3qISnDpUUIE,1086
94
94
  primitive/utils/exceptions.py,sha256=DrYHTcCAJGC7cCUwOx_FmdlVLWRdpzvDvpLb82heppE,311
95
+ primitive/utils/logging.py,sha256=vpwu-hByZC1BgJfUi6iSfAxzCobP_zg9-99EUf80KtY,1132
95
96
  primitive/utils/memory_size.py,sha256=4xfha21kW82nFvOTtDFx9Jk2ZQoEhkfXii-PGNTpIUk,3058
96
97
  primitive/utils/printer.py,sha256=f1XUpqi5dkTL3GWvYRUGlSwtj2IxU1q745T4Fxo7Tn4,370
97
98
  primitive/utils/shell.py,sha256=Z4zxmOaSyGCrS0D6I436iQci-ewHLt4UxVg1CD9Serc,2171
98
99
  primitive/utils/text.py,sha256=XiESMnlhjQ534xE2hMNf08WehE1SKaYFRNih0MmnK0k,829
99
- primitive-0.2.35.dist-info/METADATA,sha256=PLtjAKPLgi5K7IkgS3mqqeroGABE4fnmDTnSLYZE0P4,3569
100
- primitive-0.2.35.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
101
- primitive-0.2.35.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
102
- primitive-0.2.35.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
103
- primitive-0.2.35.dist-info/RECORD,,
100
+ primitive-0.2.37.dist-info/METADATA,sha256=r-F7XhBMe84oxARfJ9ZMbwVAcV5jQojsGrhxvWlK9E0,3569
101
+ primitive-0.2.37.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
102
+ primitive-0.2.37.dist-info/entry_points.txt,sha256=p1K8DMCWka5FqLlqP1sPek5Uovy9jq8u51gUsP-z334,48
103
+ primitive-0.2.37.dist-info/licenses/LICENSE.txt,sha256=B8kmQMJ2sxYygjCLBk770uacaMci4mPSoJJ8WoDBY_c,1098
104
+ primitive-0.2.37.dist-info/RECORD,,