codeplain 0.1.0__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.
Files changed (51) hide show
  1. codeplain-0.1.0.dist-info/METADATA +142 -0
  2. codeplain-0.1.0.dist-info/RECORD +51 -0
  3. codeplain-0.1.0.dist-info/WHEEL +5 -0
  4. codeplain-0.1.0.dist-info/entry_points.txt +2 -0
  5. codeplain-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. codeplain-0.1.0.dist-info/top_level.txt +36 -0
  7. codeplain_REST_api.py +370 -0
  8. config/__init__.py +2 -0
  9. config/system_config.yaml +27 -0
  10. file_utils.py +316 -0
  11. git_utils.py +304 -0
  12. hash_key.py +29 -0
  13. plain2code.py +218 -0
  14. plain2code_arguments.py +286 -0
  15. plain2code_console.py +107 -0
  16. plain2code_exceptions.py +45 -0
  17. plain2code_nodes.py +108 -0
  18. plain2code_read_config.py +74 -0
  19. plain2code_state.py +75 -0
  20. plain2code_utils.py +56 -0
  21. plain_spec.py +360 -0
  22. render_machine/actions/analyze_specification_ambiguity.py +50 -0
  23. render_machine/actions/base_action.py +19 -0
  24. render_machine/actions/commit_conformance_tests_changes.py +46 -0
  25. render_machine/actions/commit_implementation_code_changes.py +22 -0
  26. render_machine/actions/create_dist.py +26 -0
  27. render_machine/actions/exit_with_error.py +22 -0
  28. render_machine/actions/fix_conformance_test.py +121 -0
  29. render_machine/actions/fix_unit_tests.py +57 -0
  30. render_machine/actions/prepare_repositories.py +50 -0
  31. render_machine/actions/prepare_testing_environment.py +30 -0
  32. render_machine/actions/refactor_code.py +48 -0
  33. render_machine/actions/render_conformance_tests.py +169 -0
  34. render_machine/actions/render_functional_requirement.py +69 -0
  35. render_machine/actions/run_conformance_tests.py +44 -0
  36. render_machine/actions/run_unit_tests.py +38 -0
  37. render_machine/actions/summarize_conformance_tests.py +34 -0
  38. render_machine/code_renderer.py +50 -0
  39. render_machine/conformance_test_helpers.py +68 -0
  40. render_machine/implementation_code_helpers.py +20 -0
  41. render_machine/render_context.py +280 -0
  42. render_machine/render_types.py +36 -0
  43. render_machine/render_utils.py +92 -0
  44. render_machine/state_machine_config.py +408 -0
  45. render_machine/states.py +52 -0
  46. render_machine/triggers.py +27 -0
  47. standard_template_library/__init__.py +1 -0
  48. standard_template_library/golang-console-app-template.plain +36 -0
  49. standard_template_library/python-console-app-template.plain +32 -0
  50. standard_template_library/typescript-react-app-template.plain +22 -0
  51. system_config.py +49 -0
file_utils.py ADDED
@@ -0,0 +1,316 @@
1
+ import os
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ from liquid2 import Environment, FileSystemLoader, StrictUndefined
6
+ from liquid2.exceptions import UndefinedError
7
+
8
+ import plain_spec
9
+ from plain2code_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
10
+
11
+ BINARY_FILE_EXTENSIONS = [".pyc"]
12
+
13
+ # Dictionary mapping of file extensions to type names
14
+ FILE_EXTENSION_MAPPING = {
15
+ "": "plaintext",
16
+ ".py": "python",
17
+ ".txt": "plaintext",
18
+ ".md": "markdown",
19
+ ".ts": "typescript",
20
+ ".tsx": "typescript",
21
+ ".js": "javascript",
22
+ ".html": "HTML",
23
+ ".css": "CSS",
24
+ ".scss": "SASS/SCSS",
25
+ ".java": "java",
26
+ ".cpp": "C++",
27
+ ".c": "C",
28
+ ".cs": "C#",
29
+ ".php": "PHP",
30
+ ".rb": "Ruby",
31
+ ".go": "Go",
32
+ ".rs": "Rust",
33
+ ".swift": "Swift",
34
+ ".kt": "Kotlin",
35
+ ".sql": "SQL",
36
+ ".json": "JSON",
37
+ ".xml": "XML",
38
+ ".yaml": "YAML",
39
+ ".yml": "YAML", # YAML has two common extensions
40
+ ".sh": "Shell Script",
41
+ ".bat": "Batch File",
42
+ }
43
+
44
+
45
+ def get_file_type(file_name):
46
+
47
+ # Extract the file extension
48
+ ext = Path(file_name).suffix.lower() # Convert to lowercase to handle case-insensitive matching
49
+
50
+ # Use the dictionary to get the file type, defaulting to 'unknown' if the extension is not found
51
+ return FILE_EXTENSION_MAPPING.get(ext, "unknown")
52
+
53
+
54
+ def list_all_text_files(directory):
55
+ all_files = []
56
+ for root, dirs, files in os.walk(directory, topdown=True):
57
+ # Skip .git directory
58
+ if ".git" in dirs:
59
+ dirs.remove(".git")
60
+
61
+ modified_root = os.path.relpath(root, directory)
62
+ if modified_root == ".":
63
+ modified_root = ""
64
+
65
+ for filename in files:
66
+ if not any(filename.endswith(ending) for ending in BINARY_FILE_EXTENSIONS):
67
+ try:
68
+ with open(os.path.join(root, filename), "rb") as f:
69
+ f.read().decode("utf-8")
70
+ except UnicodeDecodeError:
71
+ print(f"WARNING! Not listing {filename} in {root}. File is not a text file. Skipping it.")
72
+ continue
73
+
74
+ all_files.append(os.path.join(modified_root, filename))
75
+
76
+ return all_files
77
+
78
+
79
+ def list_folders_in_directory(directory):
80
+ # List all items in the directory
81
+ items = os.listdir(directory)
82
+
83
+ # Filter out the folders
84
+ folders = [item for item in items if os.path.isdir(os.path.join(directory, item))]
85
+
86
+ return folders
87
+
88
+
89
+ # delete a folder and all its subfolders and files
90
+ def delete_folder(folder_name):
91
+ if os.path.exists(folder_name):
92
+ shutil.rmtree(folder_name)
93
+
94
+
95
+ def delete_files_and_subfolders(directory):
96
+ total_files_deleted = 0
97
+ total_folders_deleted = 0
98
+
99
+ # Walk the directory in reverse order (bottom-up)
100
+ for root, dirs, files in os.walk(directory, topdown=False):
101
+ # Delete files
102
+ for file in files:
103
+ file_path = os.path.join(root, file)
104
+ os.remove(file_path)
105
+ total_files_deleted += 1
106
+
107
+ # Delete directories
108
+ for dir_ in dirs:
109
+ dir_path = os.path.join(root, dir_)
110
+ os.rmdir(dir_path)
111
+ total_folders_deleted += 1
112
+
113
+
114
+ def copy_file(source_path, destination_path):
115
+ # Ensure the destination directory exists
116
+ os.makedirs(os.path.dirname(destination_path), exist_ok=True)
117
+
118
+ # Open the source file in read-binary ('rb') mode
119
+ with open(source_path, "rb") as source_file:
120
+ # Open the destination file in write-binary ('wb') mode
121
+ with open(destination_path, "wb") as destination_file:
122
+ # Copy the content from source to destination
123
+ while True:
124
+ # Read a chunk of the source file
125
+ chunk = source_file.read(4096) # Reading in chunks of 4KB
126
+ if not chunk:
127
+ break # End of file reached
128
+ # Write the chunk to the destination file
129
+ destination_file.write(chunk)
130
+
131
+
132
+ def add_current_path_if_no_path(filename):
133
+ # Extract the base name of the file (ignoring any path information)
134
+ basename = os.path.basename(filename)
135
+
136
+ # Compare the basename to the original filename
137
+ # If they are the same, there was no path information in the filename
138
+ if basename == filename:
139
+ # Prepend the current working directory
140
+ return os.path.join(os.getcwd(), filename)
141
+ # If the basename and the original filename differ, path information was present
142
+ return filename
143
+
144
+
145
+ def get_existing_files_content(build_folder, existing_files):
146
+ existing_files_content = {}
147
+ for file_name in existing_files:
148
+ with open(os.path.join(build_folder, file_name), "rb") as f:
149
+ content = f.read()
150
+ try:
151
+ existing_files_content[file_name] = content.decode("utf-8")
152
+ except UnicodeDecodeError:
153
+ print(f"WARNING! Error loading {file_name}. File is not a text file. Skipping it.")
154
+
155
+ return existing_files_content
156
+
157
+
158
+ def store_response_files(target_folder, response_files, existing_files):
159
+ for file_name in response_files:
160
+ full_file_name = os.path.join(target_folder, file_name)
161
+
162
+ if response_files[file_name] is None:
163
+ # None content indicates that the file should be deleted.
164
+ if os.path.exists(full_file_name):
165
+ os.remove(full_file_name)
166
+ existing_files.remove(file_name)
167
+ else:
168
+ print(f"WARNING! Cannot delete file! File {full_file_name} does not exist.")
169
+
170
+ continue
171
+
172
+ os.makedirs(os.path.dirname(full_file_name), exist_ok=True)
173
+
174
+ with open(full_file_name, "w") as f:
175
+ f.write(response_files[file_name])
176
+
177
+ if file_name not in existing_files:
178
+ existing_files.append(file_name)
179
+
180
+ return existing_files
181
+
182
+
183
+ def load_linked_resources(template_dirs: list[str], resources_list):
184
+ linked_resources = {}
185
+
186
+ for resource in resources_list:
187
+ resource_found = False
188
+ for template_dir in template_dirs:
189
+ file_name = resource["target"]
190
+ if file_name in linked_resources:
191
+ continue
192
+
193
+ full_file_name = os.path.join(template_dir, file_name)
194
+ if not os.path.isfile(full_file_name):
195
+ continue
196
+
197
+ with open(full_file_name, "rb") as f:
198
+ content = f.read()
199
+ try:
200
+ linked_resources[file_name] = content.decode("utf-8")
201
+ except UnicodeDecodeError:
202
+ print(
203
+ f"WARNING! Error loading {resource['text']} ({resource['target']}). File is not a text file. Skipping it."
204
+ )
205
+ resource_found = True
206
+ if not resource_found:
207
+ raise FileNotFoundError(
208
+ f"""
209
+ Resource file {resource['target']} not found. Resource files are searched in the following order (highest to lowest precedence):
210
+
211
+ 1. The directory containing your .plain file
212
+ 2. The directory specified by --template-dir (if provided)
213
+ 3. The built-in 'standard_template_library' directory
214
+
215
+ Please ensure that the resource exists in one of these locations, or specify the correct --template-dir if using custom templates.
216
+ """
217
+ )
218
+
219
+ return linked_resources
220
+
221
+
222
+ class TrackingFileSystemLoader(Plain2CodeLoaderMixin, FileSystemLoader):
223
+ def __init__(self, *args, **kwargs):
224
+ super().__init__(*args, **kwargs)
225
+ self.loaded_templates = {}
226
+
227
+ def get_source(self, environment, template_name, **kwargs):
228
+ source = super().get_source(environment, template_name, **kwargs)
229
+ self.loaded_templates[template_name] = source.source
230
+ return source
231
+
232
+
233
+ def get_loaded_templates(source_path, plain_source):
234
+ # Render the plain source with Liquid templating engine
235
+ # to identify the templates that are being loaded
236
+
237
+ liquid_loader = TrackingFileSystemLoader(source_path)
238
+ liquid_env = Environment(loader=liquid_loader, undefined=StrictUndefined)
239
+ liquid_env.tags["include"] = Plain2CodeIncludeTag(liquid_env)
240
+
241
+ liquid_env.filters["code_variable"] = plain_spec.code_variable_liquid_filter
242
+ liquid_env.filters["prohibited_chars"] = plain_spec.prohibited_chars_liquid_filter
243
+
244
+ plain_source_template = liquid_env.from_string(plain_source)
245
+ try:
246
+ plain_source = plain_source_template.render()
247
+ except UndefinedError as e:
248
+ raise Exception(f"Undefined liquid variable: {str(e)}")
249
+
250
+ return plain_source, liquid_loader.loaded_templates
251
+
252
+
253
+ def update_build_folder_with_rendered_files(build_folder, existing_files, response_files):
254
+ changed_files = set()
255
+ changed_files.update(response_files.keys())
256
+
257
+ existing_files = store_response_files(build_folder, response_files, existing_files)
258
+
259
+ return existing_files, changed_files
260
+
261
+
262
+ def copy_folder_content(source_folder, destination_folder, ignore_folders=None):
263
+ """
264
+ Recursively copy all files and folders from source_folder to destination_folder.
265
+ Uses shutil.copytree which handles all edge cases including permissions and symlinks.
266
+
267
+ Args:
268
+ source_folder: Source directory to copy from
269
+ destination_folder: Destination directory to copy to
270
+ ignore_folders: List of folder names to ignore during copy (default: empty list)
271
+ """
272
+ if ignore_folders is None:
273
+ ignore_folders = []
274
+
275
+ ignore_func = (
276
+ (lambda dir, files: [f for f in files if f in ignore_folders]) if ignore_folders else None # noqa: U100,U101
277
+ )
278
+ shutil.copytree(source_folder, destination_folder, dirs_exist_ok=True, ignore=ignore_func)
279
+
280
+
281
+ def get_template_directories(plain_file_path, custom_template_dir=None, default_template_dir=None) -> list[str]:
282
+ """Set up template search directories with specific precedence order.
283
+
284
+ The order of directories in the returned list determines template loading precedence.
285
+ Earlier indices (lower numbers) have higher precedence - the first matching template found will be used.
286
+
287
+ Precedence order (highest to lowest):
288
+ 1. Directory containing the plain file - for project-specific template overrides
289
+ 2. Custom template directory (if provided) - for shared custom templates
290
+ 3. Default template directory - for standard/fallback templates
291
+ """
292
+ template_dirs = [
293
+ os.path.dirname(plain_file_path), # Highest precedence - directory containing plain file
294
+ ]
295
+
296
+ if custom_template_dir:
297
+ template_dirs.append(os.path.abspath(custom_template_dir)) # Second highest - custom template dir
298
+
299
+ if default_template_dir:
300
+ # Add standard template directory last - lowest precedence
301
+ template_dirs.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), default_template_dir))
302
+
303
+ return template_dirs
304
+
305
+
306
+ def copy_folder_to_output(source_folder, output_folder):
307
+ """Copy source folder contents directly to the specified output folder."""
308
+ # Create output folder if it doesn't exist
309
+ os.makedirs(output_folder, exist_ok=True)
310
+
311
+ # If output folder exists, clean it first to ensure clean copy
312
+ if os.path.exists(output_folder):
313
+ delete_files_and_subfolders(output_folder)
314
+
315
+ # Copy source folder contents directly to output folder (excluding .git)
316
+ copy_folder_content(source_folder, output_folder, ignore_folders=[".git"])
git_utils.py ADDED
@@ -0,0 +1,304 @@
1
+ import os
2
+ from typing import Optional, Union
3
+
4
+ from git import Repo
5
+
6
+ import file_utils
7
+
8
+ FUNCTIONAL_REQUIREMENT_IMPLEMENTED_COMMIT_MESSAGE = (
9
+ "[Codeplain] Implemented code and unit tests for functional requirement {}"
10
+ )
11
+ REFACTORED_CODE_COMMIT_MESSAGE = "[Codeplain] Refactored code after implementing functional requirement {}"
12
+ CONFORMANCE_TESTS_PASSED_COMMIT_MESSAGE = (
13
+ "[Codeplain] Fixed issues in the implementation code identified during conformance testing"
14
+ )
15
+
16
+ # Following messages are used as checkpoints in the git history
17
+ # Changing them will break backwards compatibility so change them with care
18
+ FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE = "[Codeplain] Functional requirement ID (FRID):{} fully implemented"
19
+ INITIAL_COMMIT_MESSAGE = "[Codeplain] Initial commit"
20
+ BASE_FOLDER_COMMIT_MESSAGE = "[Codeplain] Initialize build with Base Folder content"
21
+
22
+
23
+ RENDERED_FRID_MESSAGE = "Changes related to Functional requirement ID (FRID): {}"
24
+ RENDER_ID_MESSAGE = "Render ID: {}"
25
+
26
+
27
+ class InvalidGitRepositoryError(Exception):
28
+ """Raised when the git repository is in an invalid state."""
29
+
30
+ pass
31
+
32
+
33
+ def init_git_repo(path_to_repo: Union[str, os.PathLike]) -> Repo:
34
+ """
35
+ Initializes a new git repository in the given path.
36
+ If folder does not exist, it creates it.
37
+ If the folder already exists, it deletes the content of the folder.
38
+ """
39
+ if os.path.isdir(path_to_repo):
40
+ file_utils.delete_files_and_subfolders(path_to_repo)
41
+ else:
42
+ os.makedirs(path_to_repo)
43
+
44
+ repo = Repo.init(path_to_repo)
45
+ repo.git.commit("--allow-empty", "-m", INITIAL_COMMIT_MESSAGE)
46
+
47
+ return repo
48
+
49
+
50
+ def is_dirty(repo_path: Union[str, os.PathLike]) -> bool:
51
+ """Checks if the repository is dirty."""
52
+ repo = Repo(repo_path)
53
+ return repo.is_dirty(untracked_files=True)
54
+
55
+
56
+ def add_all_files_and_commit(
57
+ repo_path: Union[str, os.PathLike], commit_message: str, frid: Optional[str] = None, render_id: Optional[str] = None
58
+ ) -> Repo:
59
+ """Adds all files to the git repository and commits them."""
60
+ repo = Repo(repo_path)
61
+ repo.git.add(".")
62
+
63
+ message = f"{commit_message}"
64
+
65
+ if frid or render_id:
66
+ message += "\n\n" + "-" * 80
67
+
68
+ if frid:
69
+ message += f"\n\n{RENDERED_FRID_MESSAGE.format(frid)}"
70
+ if render_id:
71
+ message += f"\n\n{RENDER_ID_MESSAGE.format(render_id)}"
72
+
73
+ # Check if there are any changes to commit
74
+ if not repo.is_dirty(untracked_files=True):
75
+ repo.git.commit("--allow-empty", "-m", message)
76
+ else:
77
+ repo.git.commit("-m", message)
78
+
79
+ return repo
80
+
81
+
82
+ def revert_changes(repo_path: Union[str, os.PathLike]) -> Repo:
83
+ """Reverts all changes made since the last commit."""
84
+ repo = Repo(repo_path)
85
+ repo.git.reset("--hard")
86
+ repo.git.clean("-xdf")
87
+ return repo
88
+
89
+
90
+ def revert_to_commit_with_frid(repo_path: Union[str, os.PathLike], frid: Optional[str] = None) -> Repo:
91
+ """
92
+ Finds commit with given frid mentioned in the commit message and reverts the branch to it.
93
+
94
+ If frid argument is not provided (None), repo is reverted to the initial state. In case the base folder doesn't exist,
95
+ code is reverted to the initial repo commit. Otherwise, the repo is reverted to the base folder commit.
96
+
97
+ It is expected that the repo has at least one commit related to provided frid if frid is not None.
98
+ In case the frid related commit is not found, an exception is raised.
99
+ """
100
+ repo = Repo(repo_path)
101
+
102
+ commit = _get_commit(repo, frid)
103
+
104
+ if not commit:
105
+ raise InvalidGitRepositoryError("Git repository is in an invalid state. Relevant commit could not be found.")
106
+
107
+ repo.git.reset("--hard", commit)
108
+ repo.git.clean("-xdf")
109
+ return repo
110
+
111
+
112
+ def checkout_commit_with_frid(repo_path: Union[str, os.PathLike], frid: Optional[str] = None) -> Repo:
113
+ """
114
+ Finds commit with given frid mentioned in the commit message and checks out that commit.
115
+
116
+ If frid argument is not provided (None), repo is checked out to the initial state. In case the base folder doesn't exist,
117
+ code is checked out to the initial repo commit. Otherwise, the repo is checked out to the base folder commit.
118
+
119
+ It is expected that the repo has at least one commit related to provided frid if frid is not None.
120
+ In case the frid related commit is not found, an exception is raised.
121
+ """
122
+ repo = Repo(repo_path)
123
+
124
+ commit = _get_commit(repo, frid)
125
+
126
+ if not commit:
127
+ raise InvalidGitRepositoryError("Git repository is in an invalid state. Relevant commit could not be found.")
128
+
129
+ repo.git.checkout(commit)
130
+ return repo
131
+
132
+
133
+ def checkout_previous_branch(repo_path: Union[str, os.PathLike]) -> Repo:
134
+ """
135
+ Checks out the previous branch using 'git checkout -'.
136
+
137
+ Args:
138
+ repo_path (str | os.PathLike): Path to the git repository
139
+
140
+ Returns:
141
+ Repo: The git repository object
142
+ """
143
+ repo = Repo(repo_path)
144
+ repo.git.checkout("-")
145
+ return repo
146
+
147
+
148
+ def _get_diff_dict(diff_output: str) -> dict:
149
+ diff_dict = {}
150
+ current_file = None
151
+ current_diff_lines = []
152
+
153
+ lines = diff_output.split("\n")
154
+ i = 0
155
+
156
+ while i < len(lines):
157
+ line = lines[i]
158
+
159
+ if line.startswith("diff --git"):
160
+ # Save previous file's diff if exists
161
+ if current_file and current_diff_lines:
162
+ diff_dict[current_file] = "\n".join(current_diff_lines)
163
+
164
+ # Extract file name from diff --git line
165
+ parts = line.split(" ")
166
+ if len(parts) >= 4:
167
+ # Get the b/ path (new file path)
168
+ current_file = parts[3][2:] if parts[3].startswith("b/") else parts[3]
169
+ current_diff_lines = []
170
+
171
+ # Skip the diff --git line
172
+ i += 1
173
+
174
+ # Skip the index line if it exists
175
+ while i < len(lines) and (
176
+ lines[i].startswith("index ")
177
+ or lines[i].startswith("new file mode ")
178
+ or lines[i].startswith("deleted file mode ")
179
+ ):
180
+ i += 1
181
+
182
+ continue
183
+
184
+ # Add all other lines to current diff
185
+ if current_file is not None:
186
+ current_diff_lines.append(line)
187
+
188
+ i += 1
189
+
190
+ # Don't forget the last file
191
+ if current_file and current_diff_lines:
192
+ diff_dict[current_file] = "\n".join(current_diff_lines)
193
+
194
+ return diff_dict
195
+
196
+
197
+ def diff(repo_path: Union[str, os.PathLike], previous_frid: str = None) -> dict:
198
+ """
199
+ Get the git diff between the current code state and the previous frid using git's native diff command.
200
+ If previous_frid is not provided, we try to find the commit related to the copy of the base folder.
201
+ Removes the 'diff --git' and 'index' lines to get clean unified diff format.
202
+
203
+
204
+ Args:
205
+ repo_path (str | os.PathLike): Path to the git repository
206
+ previous_frid (str): Functional requirement ID (FRID) of the previous commit
207
+
208
+ Returns:
209
+ dict: Dictionary with file names as keys and their clean diff strings as values
210
+ """
211
+ repo = Repo(repo_path)
212
+
213
+ commit = _get_commit(repo, previous_frid)
214
+
215
+ # Add all files to the index to get a clean diff
216
+ repo.git.add("-N", ".")
217
+
218
+ # Get the raw git diff output, excluding .pyc files
219
+ diff_output = repo.git.diff(commit, "--text", ":!*.pyc")
220
+
221
+ if not diff_output:
222
+ return {}
223
+
224
+ return _get_diff_dict(diff_output)
225
+
226
+
227
+ def _get_commit(repo: Repo, frid: Optional[str]) -> str:
228
+ if frid:
229
+ commit = _get_commit_with_frid(repo, frid)
230
+ else:
231
+ commit = _get_base_folder_commit(repo)
232
+ if not commit:
233
+ commit = _get_initial_commit(repo)
234
+
235
+ return commit
236
+
237
+
238
+ def _get_commit_with_frid(repo: Repo, frid: str) -> str:
239
+ """Finds commit with given frid mentioned in the commit message."""
240
+ commit = _get_commit_with_message(repo, FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format(frid))
241
+ if not commit:
242
+ raise InvalidGitRepositoryError(f"No commit with frid {frid} found.")
243
+ return commit
244
+
245
+
246
+ def _get_base_folder_commit(repo: Repo) -> str:
247
+ """Finds commit related to copy of the base folder."""
248
+ return _get_commit_with_message(repo, BASE_FOLDER_COMMIT_MESSAGE)
249
+
250
+
251
+ def _get_initial_commit(repo: Repo) -> str:
252
+ """Finds initial commit."""
253
+ return _get_commit_with_message(repo, INITIAL_COMMIT_MESSAGE)
254
+
255
+
256
+ def _get_commit_with_message(repo: Repo, message: str) -> str:
257
+ """Finds commit with given message."""
258
+ escaped_message = message.replace("[", "\\[").replace("]", "\\]")
259
+
260
+ return repo.git.rev_list(repo.active_branch.name, "--grep", escaped_message, "-n", "1")
261
+
262
+
263
+ def get_implementation_code_diff(repo_path: Union[str, os.PathLike], frid: str, previous_frid: str) -> dict:
264
+ repo = Repo(repo_path)
265
+
266
+ implementation_commit = _get_commit_with_message(repo, REFACTORED_CODE_COMMIT_MESSAGE.format(frid))
267
+ if not implementation_commit:
268
+ implementation_commit = _get_commit_with_message(
269
+ repo, FUNCTIONAL_REQUIREMENT_IMPLEMENTED_COMMIT_MESSAGE.format(frid)
270
+ )
271
+
272
+ previous_frid_commit = _get_commit(repo, previous_frid)
273
+
274
+ # Get the raw git diff output, excluding .pyc files
275
+ diff_output = repo.git.diff(previous_frid_commit, implementation_commit, "--text", ":!*.pyc")
276
+
277
+ if not diff_output:
278
+ return {}
279
+
280
+ return _get_diff_dict(diff_output)
281
+
282
+
283
+ def get_fixed_implementation_code_diff(repo_path: Union[str, os.PathLike], frid: str) -> dict:
284
+ repo = Repo(repo_path)
285
+
286
+ implementation_commit = _get_commit_with_message(repo, REFACTORED_CODE_COMMIT_MESSAGE.format(frid))
287
+ if not implementation_commit:
288
+ implementation_commit = _get_commit_with_message(
289
+ repo, FUNCTIONAL_REQUIREMENT_IMPLEMENTED_COMMIT_MESSAGE.format(frid)
290
+ )
291
+
292
+ conformance_tests_passed_commit = _get_commit_with_message(
293
+ repo, CONFORMANCE_TESTS_PASSED_COMMIT_MESSAGE.format(frid)
294
+ )
295
+ if not conformance_tests_passed_commit:
296
+ return None
297
+
298
+ # Get the raw git diff output, excluding .pyc files
299
+ diff_output = repo.git.diff(implementation_commit, conformance_tests_passed_commit, "--text", ":!*.pyc")
300
+
301
+ if not diff_output:
302
+ return {}
303
+
304
+ return _get_diff_dict(diff_output)
hash_key.py ADDED
@@ -0,0 +1,29 @@
1
+ import hashlib
2
+ import sys
3
+
4
+
5
+ def hash_api_key(api_key):
6
+ """Hash the provided API key using SHA-256 and return the hash as a hex string."""
7
+ try:
8
+ hash_object = hashlib.sha256(api_key.encode())
9
+ hex_dig = hash_object.hexdigest()
10
+ return hex_dig
11
+ except Exception as e:
12
+ error_message = f"An error occurred while hashing the API key: {str(e)}"
13
+ raise Exception(error_message)
14
+
15
+
16
+ if __name__ == "__main__":
17
+ if len(sys.argv) != 2:
18
+ print("Error: Exactly one argument must be provided for the API key.")
19
+ print(f"Usage: python {sys.argv[0]} <api_key>")
20
+ sys.exit(1)
21
+
22
+ api_key = sys.argv[1]
23
+
24
+ try:
25
+ hashed_key = hash_api_key(api_key)
26
+ print(f"Hashed API Key: {hashed_key}")
27
+ except Exception as e:
28
+ print(f"Error: {str(e)}")
29
+ sys.exit(1)