cloud-foundry 0.0.1__tar.gz → 0.0.2__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.
@@ -0,0 +1,6 @@
1
+
2
+ venv/
3
+ __pycache__
4
+ .pytest_cache
5
+ .env
6
+ temp/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cloud-foundry
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: cloud-foundry is a curated set of cloud components that can be used to assemble cloud based applications.
5
5
  Project-URL: Documentation, https://github.com/DanRepik/cloud-foundry
6
6
  Project-URL: Source, https://github.com/DanRepik/cloud-foundry
@@ -0,0 +1,2 @@
1
+ from cloud_foundry.pulumi.python_function import python_function
2
+
@@ -0,0 +1,6 @@
1
+ class ArchiveBuilder:
2
+ def hash(self) -> str:
3
+ raise NotImplementedError
4
+
5
+ def location(self) -> str:
6
+ raise NotImplementedError
@@ -0,0 +1,23 @@
1
+ from cloud_foundry.pulumi.python_function import PythonFunction
2
+ from cloud_foundry.python_archive_builder import PythonArchiveBuilder
3
+
4
+ def python_function(
5
+ self,
6
+ name: str,
7
+ sources: dict[str, str] = None,
8
+ requirements: list[str] = None,
9
+ environment: dict[str, str] = None,
10
+ ):
11
+ self.archive_builder = PythonArchiveBuilder(
12
+ name=f"{name}-archive-builder",
13
+ sources=sources,
14
+ requirements=requirements,
15
+ working_dir="temp",
16
+ )
17
+ return PythonFunction(
18
+ name=f"{name}-api-maker",
19
+ hash=self.archive_builder.hash(),
20
+ handler="app.lambda_handler",
21
+ archive_location=self.archive_builder.location(),
22
+ environment=environment,
23
+ )
@@ -0,0 +1,18 @@
1
+ def log_group(scope: Construct, name: str, retention_in_days: int = 3) -> CloudwatchLogGroup:
2
+ """
3
+ Creates a CloudWatch log group.
4
+
5
+ Args:
6
+ name (str): The name of the log group.
7
+ retention_in_days (int, optional): Default retention period in days. Defaults to 3 days.
8
+
9
+ Returns:
10
+ CloudwatchLogGroup: The created CloudWatch log group.
11
+ """
12
+ return CloudwatchLogGroup(
13
+ scope,
14
+ make_id(scope, name),
15
+ name=f"{scope.config.get('environment')}/{scope.config.get('product')}/{scope.config.get('domain')}/{name}",
16
+ retention_in_days=retention_in_days,
17
+ tags=make_tags(scope, name)
18
+ )
@@ -0,0 +1,123 @@
1
+ import pulumi
2
+ import pulumi_aws as aws
3
+ from cloud_foundry.utils.logger import logger
4
+
5
+ from cloud_foundry.python_archive_builder import PythonArchiveBuilder
6
+
7
+ log = logger(__name__)
8
+
9
+
10
+ class PythonFunction(pulumi.ComponentResource):
11
+ name: str
12
+ handler: str
13
+ lambda_: aws.lambda_.Function
14
+
15
+ def __init__(
16
+ self,
17
+ name,
18
+ *,
19
+ hash: str,
20
+ archive_location: str,
21
+ handler: str,
22
+ environment: dict[str, str]=None,
23
+ actions: list[str]=None,
24
+ opts=None
25
+ ):
26
+ super().__init__("custom:cloud_forge:PythonFunction", name, {}, opts)
27
+ self.name = name
28
+ self.handler = handler
29
+ self.environment = environment or {}
30
+ self.actions = actions or []
31
+ self.create_lambda_function(hash, archive_location)
32
+ self.register_outputs({})
33
+
34
+ def create_execution_role(self) -> aws.iam.Role:
35
+ log.debug("creating execution role")
36
+ assume_role_policy = aws.iam.get_policy_document(
37
+ statements=[
38
+ aws.iam.GetPolicyDocumentStatementArgs(
39
+ effect="Allow",
40
+ principals=[
41
+ aws.iam.GetPolicyDocumentStatementPrincipalArgs(
42
+ type="Service",
43
+ identifiers=["lambda.amazonaws.com"],
44
+ )
45
+ ],
46
+ actions=["sts:AssumeRole"],
47
+ )
48
+ ]
49
+ )
50
+
51
+ role = aws.iam.Role(
52
+ f"{self.name}-lambda-execution",
53
+ assume_role_policy=assume_role_policy.json,
54
+ opts=pulumi.ResourceOptions(parent=self)
55
+ )
56
+
57
+ aws.iam.get_policy_document(
58
+ statements=[
59
+ aws.iam.GetPolicyDocumentStatementArgs(
60
+ effect="Allow",
61
+ actions=(self.actions + [
62
+ "logs:CreateLogGroup",
63
+ "logs:CreateLogStream",
64
+ "logs:PutLogEvents",
65
+ ]),
66
+ resources=["*"],
67
+ )
68
+ ]
69
+ )
70
+
71
+ return role
72
+
73
+ def create_log_group(self) -> aws.cloudwatch.LogGroup:
74
+ log_group = aws.cloudwatch.LogGroup(
75
+ f"{self.name}-log-group",
76
+ name=f"/aws/lambda/{pulumi.get_project()}-{self.name}",
77
+ retention_in_days=3,
78
+ opts=pulumi.ResourceOptions(parent=self)
79
+ )
80
+ return log_group
81
+
82
+ def invoke_arn(self) -> pulumi.Output[str]:
83
+ return self.lambda_.invoke_arn
84
+
85
+ def create_lambda_function(self, hash: str, archive_location: str):
86
+ log.debug("creating lambda function")
87
+
88
+ log_group = self.create_log_group()
89
+
90
+ execution_role = self.create_execution_role()
91
+
92
+ self.lambda_ = aws.lambda_.Function(
93
+ f"{self.name}-lambda",
94
+ code=pulumi.FileArchive(archive_location),
95
+ name=f"{pulumi.get_project()}-{self.name}",
96
+ role=execution_role.arn,
97
+ handler=self.handler,
98
+ source_code_hash=hash,
99
+ runtime=aws.lambda_.Runtime.PYTHON3D9,
100
+ environment=aws.lambda_.FunctionEnvironmentArgs(variables=self.environment),
101
+ opts=pulumi.ResourceOptions(depends_on=[execution_role, log_group], parent=self),
102
+ )
103
+
104
+ def python_function(
105
+ name: str, *,
106
+ handler: str,
107
+ sources: dict[str, str] = None,
108
+ requirements: list[str] = None,
109
+ environment: dict[str, str] = None,
110
+ ):
111
+ archive_builder = PythonArchiveBuilder(
112
+ name=f"{name}-archive-builder",
113
+ sources=sources,
114
+ requirements=requirements,
115
+ working_dir="temp",
116
+ )
117
+ return PythonFunction(
118
+ name=name,
119
+ hash=archive_builder.hash(),
120
+ handler=handler,
121
+ archive_location=archive_builder.location(),
122
+ environment=environment,
123
+ )
@@ -0,0 +1,16 @@
1
+ import pulumi
2
+ import pulumi_aws as aws
3
+
4
+
5
+ class GatewayAPI(pulumi.ComponentResource):
6
+ rest_api: aws.apigateway.RestApi
7
+
8
+ def __init__(self, name, body: str, opts=None):
9
+ super().__init__("cloud_forge:apigw:RestAPI", name, None, opts)
10
+
11
+ self.rest_api = aws.apigateway.RestApi(
12
+ f"{name}-http-api", name=f"{name}-http-api", body=body, opts=opts
13
+ )
14
+
15
+ def id(self) -> pulumi.Output[str]:
16
+ return self.rest_api.id
@@ -0,0 +1,198 @@
1
+ import os
2
+ import shutil
3
+ import subprocess
4
+ import sys
5
+
6
+ from cloud_foundry.utils.logger import logger
7
+ from cloud_foundry.utils.hash_comparator import HashComparator
8
+ from cloud_foundry.archive_builder import ArchiveBuilder
9
+
10
+ log = logger(__name__)
11
+
12
+
13
+ class PythonArchiveBuilder(ArchiveBuilder):
14
+ _hash: str
15
+ _location: str
16
+
17
+ def __init__(
18
+ self,
19
+ name: str,
20
+ *,
21
+ sources: dict[str, str],
22
+ requirements: list[str],
23
+ working_dir: str,
24
+ ):
25
+ self.name = name
26
+ self._sources = sources
27
+ self._requirements = requirements
28
+ self._working_dir = working_dir
29
+
30
+ self.prepare()
31
+
32
+ hash_comparator = HashComparator()
33
+ new_hash = hash_comparator.hash_folder(self._staging)
34
+ old_hash = hash_comparator.read(self._base_dir)
35
+ log.debug(f"old_hash: {old_hash}, new_hash: {new_hash}")
36
+ if old_hash == new_hash:
37
+ self._hash = old_hash or ""
38
+ else:
39
+ self.install_requirements()
40
+ self.build_archive()
41
+ self._hash = new_hash
42
+ hash_comparator.write(self._hash, self._base_dir)
43
+
44
+ def hash(self) -> str:
45
+ return self._hash
46
+
47
+ def location(self) -> str:
48
+ return self._location
49
+
50
+ def prepare(self):
51
+ self._base_dir = os.path.join(self._working_dir, f"{self.name}-lambda")
52
+ self._staging = os.path.join(self._base_dir, "staging")
53
+ self._libs = os.path.join(self._base_dir, "libs")
54
+ self._location = os.path.join(self._base_dir, f"{self.name}.zip")
55
+
56
+ self.create_clean_folder(self._staging)
57
+ self.create_clean_folder(self._libs)
58
+
59
+ self.install_sources()
60
+ self.write_requirements()
61
+
62
+ def build_archive(self):
63
+ log.info(f"building archive: {self.name}")
64
+ try:
65
+ shutil.make_archive(
66
+ base_name=self._location.replace('.zip', ''),
67
+ format='zip',
68
+ root_dir=self._base_dir
69
+ )
70
+ log.info("Archive built successfully")
71
+ except Exception as e:
72
+ log.error(f"Error building archive: {e}")
73
+ raise
74
+
75
+ def install_sources(self):
76
+ log.info(f"installing resources: {self.name}")
77
+ if not self._sources:
78
+ return
79
+ log.debug(f"sources: {self._sources}")
80
+ for destination, source in self._sources.items():
81
+ destination_path = os.path.join(self._staging, destination)
82
+
83
+ try:
84
+ if os.path.isdir(source):
85
+ shutil.copytree(source, destination_path)
86
+ log.info(f"Folder copied from {source} to {destination_path}")
87
+ elif os.path.isfile(source):
88
+ os.makedirs(os.path.dirname(destination_path), exist_ok=True)
89
+ shutil.copy2(source, destination_path)
90
+ log.info(f"File copied from {source} to {destination_path}")
91
+ else: # inline
92
+ try:
93
+ with open(destination_path, "w") as f:
94
+ f.write(source + "\n")
95
+ except Exception as e:
96
+ log.error(
97
+ "Error writing requirements to "
98
+ + f"{destination_path}: {e}"
99
+ )
100
+ raise
101
+ except Exception as e:
102
+ log.error(f"Error copying {source} to {destination_path}: {e}")
103
+ raise
104
+
105
+ def write_requirements(self):
106
+ log.debug("writing requirements")
107
+ if not self._requirements:
108
+ return
109
+
110
+ requirements_path = os.path.join(self._staging, "requirements.txt")
111
+ try:
112
+ with open(requirements_path, "w") as f:
113
+ for requirement in self._requirements:
114
+ f.write(requirement + "\n")
115
+ except Exception as e:
116
+ log.error(f"Error writing requirements to {requirements_path}: {e}")
117
+ raise
118
+
119
+ def install_requirements(self):
120
+ log.info(f"installing packages {self.name}")
121
+ requirements_file = os.path.join(self._staging, "requirements.txt")
122
+ if not os.path.exists(requirements_file):
123
+ log.warning(f"No requirements file found at {requirements_file}")
124
+ return
125
+
126
+ log.info(
127
+ f"Installing packages using: {sys.executable} -m pip3 "
128
+ + f"install --target {self._libs} --platform manylinux2010_x86_64 "
129
+ + "--implementation cp --only-binary=:all: --upgrade "
130
+ + f"--python-version 3.9 -r {requirements_file}"
131
+ )
132
+ self.clean_folder(self._libs)
133
+
134
+ try:
135
+ subprocess.check_call(
136
+ [
137
+ sys.executable,
138
+ "-m",
139
+ "pip",
140
+ "install",
141
+ "-v",
142
+ "--target",
143
+ self._libs,
144
+ "--platform",
145
+ "manylinux2010_x86_64",
146
+ "--implementation",
147
+ "cp",
148
+ "--only-binary=:all:",
149
+ "--upgrade",
150
+ "--python-version",
151
+ "3.9",
152
+ "-r",
153
+ requirements_file,
154
+ ]
155
+ )
156
+ except subprocess.CalledProcessError as e:
157
+ log.error(f"Error installing requirements: {e}")
158
+ raise
159
+
160
+ def create_clean_folder(self, folder_path):
161
+ """
162
+ Create a clean folder by removing existing contents or creating
163
+ the folder if it doesn't exist.
164
+
165
+ Args:
166
+ folder_path (str): Path to the folder to clean or create.
167
+
168
+ Returns:
169
+ None
170
+ """
171
+ if os.path.exists(folder_path):
172
+ self.clean_folder(folder_path)
173
+ else:
174
+ os.makedirs(folder_path)
175
+
176
+ def clean_folder(self, folder_path):
177
+ """
178
+ Remove all files and folders from the specified folder.
179
+
180
+ Args:
181
+ folder_path (str): Path to the folder from which to remove
182
+ files and folders.
183
+
184
+ Returns:
185
+ None
186
+ """
187
+ log.info(f"Cleaning folder: {folder_path}")
188
+ try:
189
+ for item in os.listdir(folder_path):
190
+ item_path = os.path.join(folder_path, item)
191
+ if os.path.isfile(item_path):
192
+ os.remove(item_path)
193
+ elif os.path.isdir(item_path):
194
+ shutil.rmtree(item_path)
195
+ log.info(f"All files and folders removed from {folder_path}")
196
+ except Exception as e:
197
+ log.error(f"Error cleaning folder {folder_path}: {e}")
198
+ raise
@@ -0,0 +1,100 @@
1
+ import hashlib
2
+ import os
3
+ import re
4
+ from cloud_foundry.utils.logger import logger
5
+
6
+ log = logger(__name__)
7
+
8
+ class HashComparator:
9
+ def __init__(self, hash_algorithm="sha256"):
10
+ self.hash_algorithm = hash_algorithm
11
+
12
+ def check_folder(self, hash_file: str, hash_dir: str):
13
+ """Compares the stored hash with the current hash of the directory."""
14
+ old_hash = self.read(hash_file)
15
+ log.info(f"old_hash: {old_hash}")
16
+ if old_hash is None:
17
+ return False
18
+
19
+ new_hash = self.hash_folder(hash_dir)
20
+ log.info(f"new_hash: {new_hash}")
21
+ return self.compare(old_hash, new_hash)
22
+
23
+ def hash_file(self, file_path):
24
+ """Efficiently calculate the hash value of a file using streaming."""
25
+ hasher = hashlib.new(self.hash_algorithm)
26
+ with open(file_path, "rb") as f:
27
+ while chunk := f.read(4096):
28
+ hasher.update(chunk)
29
+ return hasher.hexdigest()
30
+
31
+ def hash_folder(self, folder_path, include_regex=None, exclude_regex=None, include_metadata=False):
32
+ """
33
+ Calculates a hash of the folder's contents (including its files and subfolders).
34
+
35
+ Args:
36
+ folder_path (str): Path to the folder to hash.
37
+ include_regex (str): Optional regex to include files.
38
+ exclude_regex (str): Optional regex to exclude files.
39
+ include_metadata (bool): If True, include file metadata (e.g., modification time) in the hash.
40
+ """
41
+ hasher = hashlib.new(self.hash_algorithm)
42
+ include_pattern = re.compile(include_regex) if include_regex else None
43
+ exclude_pattern = re.compile(exclude_regex) if exclude_regex else None
44
+
45
+ for root, dirs, files in os.walk(folder_path):
46
+ # Sort directories and files to ensure deterministic hash
47
+ dirs.sort()
48
+ files.sort()
49
+
50
+ for file in files:
51
+ file_path = os.path.join(root, file)
52
+
53
+ # Check if the file should be included or excluded
54
+ if include_pattern and not include_pattern.match(file):
55
+ continue
56
+ if exclude_pattern and exclude_pattern.match(file):
57
+ continue
58
+
59
+ # Update the folder hash with the file path and file content
60
+ relative_path = os.path.relpath(file_path, folder_path)
61
+ hasher.update(relative_path.encode()) # Update the hash with the file path
62
+
63
+ if include_metadata:
64
+ # Include metadata such as file modification time, size, and permissions
65
+ stat = os.stat(file_path)
66
+ hasher.update(str(stat.st_mtime).encode()) # Modification time
67
+ hasher.update(str(stat.st_size).encode()) # File size
68
+ hasher.update(str(stat.st_mode).encode()) # File permissions
69
+
70
+ # Hash file content and update the folder hash
71
+ with open(file_path, "rb") as f:
72
+ while chunk := f.read(4096):
73
+ hasher.update(chunk)
74
+
75
+ return hasher.hexdigest()
76
+
77
+ def read(self, file_path):
78
+ """Read the hash value from a file (expected to be stored in a `.hash` file)."""
79
+ log.debug(f"reading hash: {file_path}")
80
+ hash_file = os.path.join(file_path, ".hash")
81
+
82
+ if not os.path.exists(hash_file):
83
+ return None
84
+
85
+ try:
86
+ with open(hash_file, "r") as f:
87
+ return f.read().strip()
88
+ except FileNotFoundError:
89
+ log.debug(f"hash file not found: {hash_file}")
90
+ return None
91
+
92
+ def write(self, hash_value, file_path):
93
+ """Write the hash value to a `.hash` file in the specified directory."""
94
+ log.debug(f"writing hash: {file_path}")
95
+ with open(os.path.join(file_path, ".hash"), "w") as f:
96
+ f.write(hash_value)
97
+
98
+ def compare(self, hash_value1, hash_value2):
99
+ """Compares two hash values."""
100
+ return hash_value1 == hash_value2
@@ -0,0 +1,60 @@
1
+ import logging
2
+ import os
3
+
4
+ # Configuring the logging module with basic settings, including format and log level,
5
+ # where the log level is obtained from the environment variable LOGGING_LEVEL
6
+ # with a default of DEBUG, and force=True to ensure the configuration is applied immediately.
7
+ logging.basicConfig(
8
+ format="%(name)s:%(lineno)s - %(levelname)s - %(message)s",
9
+ level=os.getenv("LOGGING_LEVEL", "DEBUG").upper(),
10
+ force=True,
11
+ )
12
+
13
+ WARN = logging.WARN
14
+ INFO = logging.INFO
15
+ DEBUG = logging.DEBUG
16
+
17
+
18
+ def logger(name=None):
19
+ """
20
+ Function to create a logger with a specified name or default name.
21
+
22
+ Parameters:
23
+ name (str): Name of the logger. If not provided, the root logger is returned.
24
+
25
+ Returns:
26
+ logging.Logger: Logger object with the specified name or the root logger.
27
+
28
+ """
29
+ # Retrieving the logging level from the environment variable LOGGING_LEVEL
30
+ # with a default of DEBUG, and converting it to uppercase
31
+ loggingLevel = os.getenv("LOGGING_LEVEL", "DEBUG").upper()
32
+
33
+ # Setting the logging level for the root logger to the obtained logging level
34
+ logging.getLogger().setLevel(loggingLevel)
35
+
36
+ # Returning a logger object with the specified name or the root logger
37
+ return logging.getLogger(name)
38
+
39
+
40
+ def write_logging_file(file_name, content):
41
+ """
42
+ Function to write a given string to a file in the temp/logging folder.
43
+
44
+ Parameters:
45
+ file_name (str): Name of the file to write the content to.
46
+ content (str): The string content to write to the file.
47
+
48
+ """
49
+ # Define the directory path
50
+ dir_path = os.path.join("temp", "logging")
51
+
52
+ # Ensure the directory exists
53
+ os.makedirs(dir_path, exist_ok=True)
54
+
55
+ # Define the file path
56
+ file_path = os.path.join(dir_path, file_name)
57
+
58
+ # Write the content to the file
59
+ with open(file_path, "w") as file:
60
+ file.write(content + "\n")
@@ -1,10 +1,12 @@
1
+ # src/pyproject.tomal
2
+
1
3
  [build-system]
2
4
  requires = ["hatchling>=1.0"]
3
5
  build-backend = "hatchling.build"
4
6
 
5
7
  [project]
6
8
  name = "cloud-foundry"
7
- version = "0.0.1"
9
+ version = "0.0.2"
8
10
  authors = [
9
11
  { name="Daniel Repik", email="danrepik@icloud.com" },
10
12
  ]
@@ -24,10 +26,10 @@ dependencies = [
24
26
  "Source" = "https://github.com/DanRepik/cloud-foundry"
25
27
 
26
28
  [tool.hatch.build.targets.sdist]
27
- include = ["cloud-foundry"]
29
+ include = ["cloud_foundry"]
28
30
 
29
31
  [tool.hatch.build.targets.wheel]
30
- include = ["cloud-foundry"]
32
+ include = ["cloud_foundry"]
31
33
 
32
34
  [tool.pytest.ini_options]
33
35
  markers = [
File without changes