Skip to content

Commit 34b3e2d

Browse files
authored
New folder project structure (#82)
* New project folder structure.
1 parent 7c2045b commit 34b3e2d

21 files changed

+668
-604
lines changed

src/event_gate_lambda.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,8 @@
3131
from jsonschema import validate
3232
from jsonschema.exceptions import ValidationError
3333

34-
# Import writer modules with explicit ImportError fallback
35-
try:
36-
from . import writer_eventbridge
37-
from . import writer_kafka
38-
from . import writer_postgres
39-
except ImportError: # fallback when executed outside package context
40-
import writer_eventbridge # type: ignore[no-redef]
41-
import writer_kafka # type: ignore[no-redef]
42-
import writer_postgres # type: ignore[no-redef]
43-
44-
# Import configuration directory symbols with explicit ImportError fallback
45-
try:
46-
from .conf_path import CONF_DIR, INVALID_CONF_ENV # type: ignore[no-redef]
47-
except ImportError: # fallback when executed outside package context
48-
from conf_path import CONF_DIR, INVALID_CONF_ENV # type: ignore[no-redef]
34+
from src.writers import writer_eventbridge, writer_kafka, writer_postgres
35+
from src.utils.conf_path import CONF_DIR, INVALID_CONF_ENV
4936

5037
# Internal aliases used by rest of module
5138
_CONF_DIR = CONF_DIR

src/utils/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#
2+
# Copyright 2025 ABSA Group Limited
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#

src/conf_path.py renamed to src/utils/conf_path.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"""Module providing reusable configuration directory resolution.
1818
Resolution order:
1919
1. CONF_DIR env var if it exists and points to a directory
20-
2. <project_root>/conf (project_root = parent of this file's directory)
20+
2. <project_root>/conf
2121
3. <this_module_dir>/conf (flattened deployment)
2222
4. Fallback to <project_root>/conf even if missing (subsequent file operations will raise)
2323
"""
@@ -34,9 +34,12 @@ def resolve_conf_dir(env_var: str = "CONF_DIR"):
3434
Tuple (conf_dir, invalid_env) where conf_dir is the chosen directory path and
3535
invalid_env is the rejected env var path if provided but invalid, else None.
3636
"""
37-
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
37+
# Simplified project root: two levels up from this file (../../)
38+
parent_utils_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
39+
project_root = os.path.abspath(os.path.join(parent_utils_dir, ".."))
3840
current_dir = os.path.dirname(__file__)
3941

42+
# Scenario 1: Environment variable if it exists and points to a directory
4043
env_conf = os.environ.get(env_var)
4144
invalid_env = None
4245
conf_dir = None
@@ -48,16 +51,19 @@ def resolve_conf_dir(env_var: str = "CONF_DIR"):
4851
else:
4952
invalid_env = candidate
5053

54+
# Scenario 2: Use <project_root>/conf if present and not already satisfied by env var
5155
if conf_dir is None:
5256
parent_conf = os.path.join(project_root, "conf")
5357
if os.path.isdir(parent_conf):
5458
conf_dir = parent_conf
5559

60+
# Scenario 3: Use <this_module_dir>/conf for flattened deployments.
5661
if conf_dir is None:
5762
current_conf = os.path.join(current_dir, "conf")
5863
if os.path.isdir(current_conf):
5964
conf_dir = current_conf
6065

66+
# Scenario 4: Final fallback to <project_root>/conf even if it does not exist.
6167
if conf_dir is None:
6268
conf_dir = os.path.join(project_root, "conf")
6369

File renamed without changes.
File renamed without changes.

src/trace_logging.py renamed to src/utils/trace_logging.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
import logging
2323
from typing import Any, Dict
2424

25-
from .logging_levels import TRACE_LEVEL
26-
from .safe_serialization import safe_serialize_for_log
25+
from src.utils.logging_levels import TRACE_LEVEL
26+
from src.utils.safe_serialization import safe_serialize_for_log
2727

2828

2929
def log_payload_at_trace(logger: logging.Logger, writer_name: str, topic_name: str, message: Dict[str, Any]) -> None:

src/writers/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#
2+
# Copyright 2025 ABSA Group Limited
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
Lines changed: 99 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,99 @@
1-
#
2-
# Copyright 2025 ABSA Group Limited
3-
#
4-
# Licensed under the Apache License, Version 2.0 (the "License");
5-
# you may not use this file except in compliance with the License.
6-
# You may obtain a copy of the License at
7-
#
8-
# http://www.apache.org/licenses/LICENSE-2.0
9-
#
10-
# Unless required by applicable law or agreed to in writing, software
11-
# distributed under the License is distributed on an "AS IS" BASIS,
12-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
# See the License for the specific language governing permissions and
14-
# limitations under the License.
15-
#
16-
17-
"""EventBridge writer module.
18-
19-
Provides initialization and write functionality for publishing events to AWS EventBridge.
20-
"""
21-
22-
import json
23-
import logging
24-
from typing import Any, Dict, Optional, Tuple, List
25-
26-
import boto3
27-
from botocore.exceptions import BotoCoreError, ClientError
28-
29-
from .trace_logging import log_payload_at_trace
30-
31-
STATE: Dict[str, Any] = {"logger": logging.getLogger(__name__), "event_bus_arn": "", "client": None}
32-
33-
34-
def init(logger: logging.Logger, config: Dict[str, Any]) -> None:
35-
"""Initialize the EventBridge writer.
36-
37-
Args:
38-
logger: Shared application logger.
39-
config: Configuration dictionary (expects optional 'event_bus_arn').
40-
"""
41-
STATE["logger"] = logger
42-
STATE["client"] = boto3.client("events")
43-
STATE["event_bus_arn"] = config.get("event_bus_arn", "")
44-
STATE["logger"].debug("Initialized EVENTBRIDGE writer")
45-
46-
47-
def _format_failed_entries(entries: List[Dict[str, Any]]) -> str:
48-
failed = [e for e in entries if "ErrorCode" in e or "ErrorMessage" in e]
49-
# Keep message concise but informative
50-
return json.dumps(failed) if failed else "[]"
51-
52-
53-
def write(topic_name: str, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
54-
"""Publish a message to EventBridge.
55-
56-
Args:
57-
topic_name: Source topic name used as event Source.
58-
message: JSON-serializable payload.
59-
Returns:
60-
Tuple of success flag and optional error message.
61-
"""
62-
logger = STATE["logger"]
63-
event_bus_arn = STATE["event_bus_arn"]
64-
client = STATE["client"]
65-
66-
if not event_bus_arn:
67-
logger.debug("No EventBus Arn - skipping")
68-
return True, None
69-
if client is None: # defensive
70-
logger.debug("EventBridge client not initialized - skipping")
71-
return True, None
72-
73-
log_payload_at_trace(logger, "EventBridge", topic_name, message)
74-
75-
try:
76-
logger.debug("Sending to eventBridge %s", topic_name)
77-
response = client.put_events(
78-
Entries=[
79-
{
80-
"Source": topic_name,
81-
"DetailType": "JSON",
82-
"Detail": json.dumps(message),
83-
"EventBusName": event_bus_arn,
84-
}
85-
]
86-
)
87-
failed_count = response.get("FailedEntryCount", 0)
88-
if failed_count > 0:
89-
entries = response.get("Entries", [])
90-
failed_repr = _format_failed_entries(entries)
91-
msg = f"{failed_count} EventBridge entries failed: {failed_repr}"
92-
logger.error(msg)
93-
return False, msg
94-
except (BotoCoreError, ClientError) as err: # explicit AWS client-related errors
95-
logger.exception("EventBridge put_events call failed")
96-
return False, str(err)
97-
98-
# Let any unexpected exception propagate for upstream handler (avoids broad except BLE001 / TRY400)
99-
return True, None
1+
#
2+
# Copyright 2025 ABSA Group Limited
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
"""EventBridge writer module.
18+
19+
Provides initialization and write functionality for publishing events to AWS EventBridge.
20+
"""
21+
22+
import json
23+
import logging
24+
from typing import Any, Dict, Optional, Tuple, List
25+
26+
import boto3
27+
from botocore.exceptions import BotoCoreError, ClientError
28+
29+
from src.utils.trace_logging import log_payload_at_trace
30+
31+
STATE: Dict[str, Any] = {"logger": logging.getLogger(__name__), "event_bus_arn": "", "client": None}
32+
33+
34+
def init(logger: logging.Logger, config: Dict[str, Any]) -> None:
35+
"""Initialize the EventBridge writer.
36+
37+
Args:
38+
logger: Shared application logger.
39+
config: Configuration dictionary (expects optional 'event_bus_arn').
40+
"""
41+
STATE["logger"] = logger
42+
STATE["client"] = boto3.client("events")
43+
STATE["event_bus_arn"] = config.get("event_bus_arn", "")
44+
STATE["logger"].debug("Initialized EVENTBRIDGE writer")
45+
46+
47+
def _format_failed_entries(entries: List[Dict[str, Any]]) -> str:
48+
failed = [e for e in entries if "ErrorCode" in e or "ErrorMessage" in e]
49+
# Keep message concise but informative
50+
return json.dumps(failed) if failed else "[]"
51+
52+
53+
def write(topic_name: str, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
54+
"""Publish a message to EventBridge.
55+
56+
Args:
57+
topic_name: Source topic name used as event Source.
58+
message: JSON-serializable payload.
59+
Returns:
60+
Tuple of success flag and optional error message.
61+
"""
62+
logger = STATE["logger"]
63+
event_bus_arn = STATE["event_bus_arn"]
64+
client = STATE["client"]
65+
66+
if not event_bus_arn:
67+
logger.debug("No EventBus Arn - skipping")
68+
return True, None
69+
if client is None: # defensive
70+
logger.debug("EventBridge client not initialized - skipping")
71+
return True, None
72+
73+
log_payload_at_trace(logger, "EventBridge", topic_name, message)
74+
75+
try:
76+
logger.debug("Sending to eventBridge %s", topic_name)
77+
response = client.put_events(
78+
Entries=[
79+
{
80+
"Source": topic_name,
81+
"DetailType": "JSON",
82+
"Detail": json.dumps(message),
83+
"EventBusName": event_bus_arn,
84+
}
85+
]
86+
)
87+
failed_count = response.get("FailedEntryCount", 0)
88+
if failed_count > 0:
89+
entries = response.get("Entries", [])
90+
failed_repr = _format_failed_entries(entries)
91+
msg = f"{failed_count} EventBridge entries failed: {failed_repr}"
92+
logger.error(msg)
93+
return False, msg
94+
except (BotoCoreError, ClientError) as err: # explicit AWS client-related errors
95+
logger.exception("EventBridge put_events call failed")
96+
return False, str(err)
97+
98+
# Let any unexpected exception propagate for upstream handler (avoids broad except BLE001 / TRY400)
99+
return True, None

0 commit comments

Comments
 (0)