Skip to content

Commit 6c61b4d

Browse files
author
Tiago Seabra
committed
feat: release changes for version 0.4.10
1 parent 1c60be9 commit 6c61b4d

File tree

18 files changed

+836
-122
lines changed

18 files changed

+836
-122
lines changed

galaxy/api/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
import importlib
23
import logging
34
from datetime import datetime
@@ -12,7 +13,7 @@
1213
from fastapi import Depends, FastAPI, HTTPException
1314

1415
from galaxy.core.galaxy import Integration, run_integration
15-
from galaxy.core.logging import get_log_format
16+
from galaxy.core.logging import get_log_format, get_magneto_logs
1617
from galaxy.core.magneto import Magneto
1718
from galaxy.core.mapper import Mapper
1819
from galaxy.core.models import SchedulerJobStates
@@ -56,8 +57,12 @@ def job_listener(event):
5657
scheduler.add_listener(job_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR | EVENT_JOB_SUBMITTED | EVENT_JOB_REMOVED)
5758

5859
async def _run_integration():
60+
if (log_handler := get_magneto_logs(logger)) is not None:
61+
log_handler.logs.clear()
62+
63+
fresh_instance = copy.deepcopy(instance)
5964
async with Magneto(instance.config.rely.url, instance.config.rely.token, logger=logger) as magneto_client:
60-
success = await run_integration(instance, magneto_client=magneto_client, logger=app.state.logger)
65+
success = await run_integration(fresh_instance, magneto_client=magneto_client, logger=logger)
6166
if success:
6267
logger.info("Integration %r run completed successfully: %r", instance.type_, instance.id_)
6368
else:
Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
1+
from logging import Logger
2+
13
from fastapi import APIRouter, Depends
24

35
from galaxy.core.magneto import Magneto
46
from galaxy.core.mapper import Mapper
57
from galaxy.core.utils import get_mapper, get_logger, get_magneto_client
68

7-
import logging
8-
99
router = APIRouter(prefix="/{{cookiecutter.integration_name}}", tags=["{{cookiecutter.integration_name}}"])
1010

1111

1212
@router.post("/webhook")
1313
async def {{cookiecutter.integration_name}}_webhook(
1414
event: dict,
1515
mapper: Mapper = Depends(get_mapper),
16-
logger: logging = Depends(get_logger),
16+
logger: Logger = Depends(get_logger),
1717
magneto_client: Magneto = Depends(get_magneto_client),
18-
) -> dict:
19-
entity = await mapper.process("entities", [event])
20-
logger.info(f"Received entity: {entity}")
21-
return {"message": "received gitlab webhook"}
18+
) -> None:
19+
try:
20+
entity, *_ = await mapper.process("entities", [event])
21+
logger.info("Received entity: %s", entity)
22+
except Exception:
23+
...
24+
return
25+
26+
try:
27+
await magneto_client.upsert_entity(entity)
28+
except Exception:
29+
...

galaxy/core/mapper.py

Lines changed: 72 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,66 @@
1-
import asyncio
21
import re
32
from typing import Any
43

54
import jq
65
import yaml
76

87
from galaxy.core.resources import load_integration_resource
8+
from galaxy.utils.concurrency import run_in_thread
99

10-
__all__ = ["Mapper"]
10+
__all__ = ["Mapper", "MapperError", "MapperNotFoundError", "MapperCompilationError"]
1111

1212

1313
class Mapper:
14+
MAPPINGS_FILE_PATH: str = ".rely/mappings.yaml"
15+
1416
def __init__(self, integration_name: str):
1517
self.integration_name = integration_name
1618
self.id_allowed_chars = "[^a-zA-Z0-9-]"
1719

18-
async def _load_mapping(self, mapping_kind: str) -> list[dict]:
19-
mappings = yaml.safe_load(load_integration_resource(self.integration_name, ".rely/mappings.yaml"))
20-
return [mapping for mapping in mappings.get("resources") if mapping["kind"] == mapping_kind]
21-
22-
def _compile_mappings(self, mapping: dict) -> dict:
23-
compiled_mapping = {}
24-
for key, value in mapping.items():
25-
if isinstance(value, dict):
26-
compiled_mapping[key] = self._compile_mappings(value)
27-
elif isinstance(value, list):
28-
compiled_mapping[key] = [
29-
self._compile_mappings(item) if isinstance(item, dict) else item for item in value
30-
]
31-
else:
32-
try:
33-
compiled_mapping[key] = jq.compile(value) if isinstance(value, str) else value
34-
except Exception as e:
35-
raise Exception(f"Error compiling maps for key {key} with expression {value}: {e}")
20+
self._mappings: dict[str, dict[str, Any]] | None = None
21+
self._compiled_mappings: dict[str, dict[str, Any]] = {}
22+
23+
@property
24+
def mappings(self) -> dict[str, dict[str, Any]]:
25+
if self._mappings is None:
26+
mappings = yaml.safe_load(load_integration_resource(self.integration_name, self.MAPPINGS_FILE_PATH))
27+
self._mappings = {mapping["kind"]: mapping["mappings"] for mapping in mappings.get("resources") or []}
28+
return self._mappings
29+
30+
def get_compiled_mappings(self, mapping_kind: str) -> list[Any]:
31+
if mapping_kind not in self._compiled_mappings:
32+
try:
33+
self._compiled_mappings[mapping_kind] = self._compile_mappings(self.mappings.get(mapping_kind) or {})
34+
except Exception as e:
35+
raise MapperCompilationError(mapping_kind) from e
36+
return self._compiled_mappings[mapping_kind]
37+
38+
def _compile_mappings(self, item: Any) -> Any:
39+
if isinstance(item, dict):
40+
return {key: self._compile_mappings(value) for key, value in item.items()}
41+
if isinstance(item, list | tuple | set):
42+
return [self._compile_mappings(value) for value in item]
43+
if isinstance(item, str):
44+
try:
45+
return jq.compile(item)
46+
except Exception as e:
47+
raise Exception(f"Error compiling maps with expression {item}: {e}") from e
48+
return item
49+
50+
def _map_data(self, compiled_mapping: Any, context: dict[str, Any]) -> Any:
51+
if isinstance(compiled_mapping, dict):
52+
return {key: self._map_data(value, context) for key, value in compiled_mapping.items()}
53+
if isinstance(compiled_mapping, list):
54+
return [self._map_data(item, context) for item in compiled_mapping]
55+
if isinstance(compiled_mapping, jq._Program):
56+
try:
57+
return compiled_mapping.input(context).first()
58+
except Exception as e:
59+
raise Exception(f"Error mapping with expression {compiled_mapping} and payload {compiled_mapping}: {e}")
3660
return compiled_mapping
3761

38-
def _map_entity(self, compiled_mapping: dict, json_data: dict) -> dict:
39-
entity = {}
40-
41-
for key, value in compiled_mapping.items():
42-
if isinstance(value, dict):
43-
entity[key] = self._map_entity(value, json_data)
44-
elif isinstance(value, list):
45-
entity[key] = [self._map_entity(item, json_data) if isinstance(item, dict) else item for item in value]
46-
else:
47-
try:
48-
entity[key] = value.input(json_data).first() if isinstance(value, jq._Program) else value
49-
except Exception as e:
50-
raise Exception(f"Error mapping key {key} with expression {value} and payload {json_data}: {e}")
51-
52-
return self._sanitize(entity)
62+
def _map_entity(self, compiled_mapping: dict, json_data: dict[str, Any]) -> dict:
63+
return self._sanitize(self._map_data(compiled_mapping, json_data))
5364

5465
def _replace_non_matching_characters(self, input_string: str, regex_pattern: str) -> str:
5566
res = re.sub(regex_pattern, ".", input_string)
@@ -77,21 +88,31 @@ def _sanitize(self, entity: dict) -> dict:
7788

7889
return entity
7990

80-
async def process(self, mapping_kind: str, json_data: list[dict], context=None) -> tuple[Any]:
81-
try:
82-
mappings = await self._load_mapping(mapping_kind)
83-
if not mappings:
84-
raise Exception(f"Unknown Mapper {mapping_kind}")
85-
compiled_mappings = self._compile_mappings(mappings[0]["mappings"])
91+
def process_sync(self, mapping_kind: str, json_data: list[dict], context: Any | None = None) -> list[Any]:
92+
mappings = self.get_compiled_mappings(mapping_kind)
93+
if not mappings:
94+
raise MapperNotFoundError(mapping_kind)
95+
return [self._map_entity(mappings, {**each, "context": context}) for each in json_data]
8696

87-
loop = asyncio.get_running_loop()
97+
async def process(self, mapping_kind: str, json_data: list[dict], context: Any | None = None) -> tuple[Any]:
98+
# There is no advantage in using async here as all the work is done in a thread.
99+
# Keeping it as async for now to avoid breaking existing code that calls this as `await mapper.process(...)`.
100+
return await run_in_thread(self.process_sync, mapping_kind, json_data, context)
88101

89-
entities = await asyncio.gather(
90-
*[
91-
loop.run_in_executor(None, self._map_entity, compiled_mappings, {**each, "context": context})
92-
for each in json_data
93-
]
94-
)
95-
return entities
96-
except Exception as e:
97-
raise e
102+
103+
class MapperError(Exception):
104+
"""Base class for Mapper errors."""
105+
106+
107+
class MapperNotFoundError(MapperError):
108+
"""Mapper not found error."""
109+
110+
def __init__(self, mapping_kind: str):
111+
super().__init__(f"Unknown Mapper {mapping_kind}")
112+
113+
114+
class MapperCompilationError(MapperError):
115+
"""Mapper compilation error."""
116+
117+
def __init__(self, mapping_kind: str):
118+
super().__init__(f"Error compiling mappings for kind {mapping_kind}")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

0 commit comments

Comments
 (0)