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.
- cloud_foundry-0.0.2/.gitignore +6 -0
- {cloud_foundry-0.0.1 → cloud_foundry-0.0.2}/PKG-INFO +1 -1
- cloud_foundry-0.0.2/cloud_foundry/__init__.py +2 -0
- cloud_foundry-0.0.2/cloud_foundry/archive_builder.py +6 -0
- cloud_foundry-0.0.2/cloud_foundry/cloud_foundry.py +23 -0
- cloud_foundry-0.0.2/cloud_foundry/pulumi/log_group.pv +18 -0
- cloud_foundry-0.0.2/cloud_foundry/pulumi/python_function.py +123 -0
- cloud_foundry-0.0.2/cloud_foundry/pulumi/rest_api.py +16 -0
- cloud_foundry-0.0.2/cloud_foundry/python_archive_builder.py +198 -0
- cloud_foundry-0.0.2/cloud_foundry/utils/hash_comparator.py +100 -0
- cloud_foundry-0.0.2/cloud_foundry/utils/logger.py +60 -0
- {cloud_foundry-0.0.1 → cloud_foundry-0.0.2}/pyproject.toml +5 -3
- {cloud_foundry-0.0.1 → cloud_foundry-0.0.2}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: cloud-foundry
|
|
3
|
-
Version: 0.0.
|
|
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,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.
|
|
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 = ["
|
|
29
|
+
include = ["cloud_foundry"]
|
|
28
30
|
|
|
29
31
|
[tool.hatch.build.targets.wheel]
|
|
30
|
-
include = ["
|
|
32
|
+
include = ["cloud_foundry"]
|
|
31
33
|
|
|
32
34
|
[tool.pytest.ini_options]
|
|
33
35
|
markers = [
|
|
File without changes
|