ruf-common 1.0.1__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Brian Ruf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: ruf-common
3
+ Version: 1.0.1
4
+ Summary: Common Python utilities
5
+ Author-email: Brian Ruf <Brian@RufRisk.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/brian-ruf/ruf-common-python
8
+ Project-URL: Repository, https://github.com/brian-ruf/ruf-common-python.git
9
+ Project-URL: Documentation, https://github.com/brian-ruf/ruf-common-python#readme
10
+ Project-URL: Issues, https://github.com/brian-ruf/ruf-common-python/issues
11
+ Keywords: utilities,common,helpers
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Operating System :: OS Independent
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: loguru>=0.7.3
20
+ Requires-Dist: elementpath>=4.7.0
21
+ Requires-Dist: pytz>=2025.2
22
+ Requires-Dist: tzlocal>=5.3.1
23
+ Requires-Dist: geopy>=2.4.1
24
+ Requires-Dist: timezonefinder>=8.1.0
25
+ Requires-Dist: aiohttp>=3.12.15
26
+ Requires-Dist: boto3
27
+ Requires-Dist: requests>=2.31.0
28
+ Requires-Dist: pycountry>=22.3.5
29
+ Requires-Dist: html2text>=2020.1.16
30
+ Requires-Dist: packaging>=23.1
31
+ Requires-Dist: pyyaml>=6.0.2
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
34
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
35
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # COMMON PYTHON MODULES
39
+
40
+ ## Overview
41
+ This is a collection of python modules I have created and use in several of my projects. This is just a convenient way for me to keep them in sync across projects. The repo is public so that it can be easily referenced by people using my public projects.
42
+
43
+ Feedback welcome in the form of a [GitHub issue](https://github.com/brian-ruf/ruf-common-python/issues). While I will try to address issues in a timely matter, I only intend to invest in feature requests that align with my project work. Feel free to contribute backward compatible enhancements.
44
+
45
+ ## Dependencies
46
+
47
+ Collectively, these modules rely on the following external libraries:
48
+
49
+ - loguru
50
+ - elementpath
51
+ - pytz
52
+ - tzlocal
53
+ - geopy
54
+ - timezonefinder
55
+ - aiohttp
56
+ - boto3
57
+ - requests
58
+ - pycountry
59
+ - html2text
60
+ - packaging
61
+ - pyyaml
62
+
63
+ ## Setup
64
+
65
+
66
+
67
+ To use this submodule in your GitHub repository:
68
+
69
+ 1. With your repository's `./src` folder as the default location, issue the following command:
70
+ ```
71
+ git submodule add https://github.com/brian-ruf/common-python.git common
72
+ ```
73
+
74
+ 2. Import the library into your python modules:
75
+
76
+ ```python
77
+ from common import * # to import all
78
+
79
+ # OR
80
+
81
+ from common import misc # import only one of the modules
82
+ ```
83
+
84
+ ## Modules
85
+
86
+ The following modules are exposed to your application via the above instructions:
87
+
88
+ - `aws.py`: Functions for interacting with AWS services
89
+ - `country_code_converter.py`: Functions for converting between country code formats
90
+ - `data.py`: Functions for managing and manipulating XML, JSON and YAML content
91
+ - `database.py`: Functions for interacting with a database. These functions operate the same for all supported databases
92
+ - `helper.py`: Various helper functions
93
+ - `html_to_markdown.py`: Functions for converting HTML content to Markdown
94
+ - `lfs.py`: Functions for interacting with the local file system (LFS)
95
+ - `logging.py`: Logging configuration and utilities
96
+ - `network.py`: Functions for network operations
97
+ - `stats.py`: Statistical helper functions
98
+ - `timezone_lookup.py`: Functions for timezone lookups based on location
99
+ - `xml_formatter.py`: Functions for formatting XML content
100
+
101
+ The following additional modules are present and support the above, but are not directly exposed:
102
+ - `database_sqlite3.py`: Any database-specific interactions are collected in a single file for that database
@@ -0,0 +1,65 @@
1
+ # COMMON PYTHON MODULES
2
+
3
+ ## Overview
4
+ This is a collection of python modules I have created and use in several of my projects. This is just a convenient way for me to keep them in sync across projects. The repo is public so that it can be easily referenced by people using my public projects.
5
+
6
+ Feedback welcome in the form of a [GitHub issue](https://github.com/brian-ruf/ruf-common-python/issues). While I will try to address issues in a timely matter, I only intend to invest in feature requests that align with my project work. Feel free to contribute backward compatible enhancements.
7
+
8
+ ## Dependencies
9
+
10
+ Collectively, these modules rely on the following external libraries:
11
+
12
+ - loguru
13
+ - elementpath
14
+ - pytz
15
+ - tzlocal
16
+ - geopy
17
+ - timezonefinder
18
+ - aiohttp
19
+ - boto3
20
+ - requests
21
+ - pycountry
22
+ - html2text
23
+ - packaging
24
+ - pyyaml
25
+
26
+ ## Setup
27
+
28
+
29
+
30
+ To use this submodule in your GitHub repository:
31
+
32
+ 1. With your repository's `./src` folder as the default location, issue the following command:
33
+ ```
34
+ git submodule add https://github.com/brian-ruf/common-python.git common
35
+ ```
36
+
37
+ 2. Import the library into your python modules:
38
+
39
+ ```python
40
+ from common import * # to import all
41
+
42
+ # OR
43
+
44
+ from common import misc # import only one of the modules
45
+ ```
46
+
47
+ ## Modules
48
+
49
+ The following modules are exposed to your application via the above instructions:
50
+
51
+ - `aws.py`: Functions for interacting with AWS services
52
+ - `country_code_converter.py`: Functions for converting between country code formats
53
+ - `data.py`: Functions for managing and manipulating XML, JSON and YAML content
54
+ - `database.py`: Functions for interacting with a database. These functions operate the same for all supported databases
55
+ - `helper.py`: Various helper functions
56
+ - `html_to_markdown.py`: Functions for converting HTML content to Markdown
57
+ - `lfs.py`: Functions for interacting with the local file system (LFS)
58
+ - `logging.py`: Logging configuration and utilities
59
+ - `network.py`: Functions for network operations
60
+ - `stats.py`: Statistical helper functions
61
+ - `timezone_lookup.py`: Functions for timezone lookups based on location
62
+ - `xml_formatter.py`: Functions for formatting XML content
63
+
64
+ The following additional modules are present and support the above, but are not directly exposed:
65
+ - `database_sqlite3.py`: Any database-specific interactions are collected in a single file for that database
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ruf-common"
7
+ version = "1.0.1"
8
+ description = "Common Python utilities"
9
+ requires-python = ">=3.9"
10
+ license = "MIT"
11
+ authors = [{ name = "Brian Ruf", email = "Brian@RufRisk.com" }]
12
+ readme = "README.md"
13
+ classifiers = [
14
+ "Development Status :: 5 - Production/Stable",
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ keywords = ["utilities", "common", "helpers"]
20
+ dependencies = [
21
+ "loguru>=0.7.3",
22
+ "elementpath>=4.7.0",
23
+ "pytz>=2025.2",
24
+ "tzlocal>=5.3.1",
25
+ "geopy>=2.4.1",
26
+ "timezonefinder>=8.1.0",
27
+ "aiohttp>=3.12.15",
28
+ "boto3",
29
+ "requests>=2.31.0",
30
+ "pycountry>=22.3.5",
31
+ "html2text>=2020.1.16",
32
+ "packaging>=23.1",
33
+ "pyyaml>=6.0.2"
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/brian-ruf/ruf-common-python"
38
+ Repository = "https://github.com/brian-ruf/ruf-common-python.git"
39
+ Documentation = "https://github.com/brian-ruf/ruf-common-python#readme"
40
+ Issues = "https://github.com/brian-ruf/ruf-common-python/issues"
41
+
42
+ [project.optional-dependencies]
43
+ dev = [
44
+ "pytest>=7.0.0",
45
+ "pytest-cov>=4.0.0",
46
+ "pytest-asyncio>=0.21.0",
47
+ ]
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["."]
51
+ include = ["ruf_common*"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ python_files = ["test_*.py"]
56
+ python_classes = ["Test*"]
57
+ python_functions = ["test_*"]
58
+ addopts = "-v --tb=short"
59
+
60
+ [tool.coverage.run]
61
+ source = ["ruf_common"]
62
+ omit = ["tests/*", "*/__pycache__/*"]
63
+
64
+ [tool.coverage.report]
65
+ exclude_lines = [
66
+ "pragma: no cover",
67
+ "if __name__ == .__main__.:",
68
+ ]
@@ -0,0 +1,27 @@
1
+ from . import data
2
+ from . import database
3
+ from . import lfs
4
+ from . import helper
5
+ from . import network
6
+ from . import aws
7
+ from . import logging
8
+ from . import stats
9
+ from . import country_code_converter
10
+ from . import html_to_markdown
11
+ from . import timezone_lookup
12
+ from . import xml_formatter
13
+
14
+ __all__ = [
15
+ "data",
16
+ "database",
17
+ "lfs",
18
+ "helper",
19
+ "network",
20
+ "aws",
21
+ "logging",
22
+ "stats",
23
+ "country_code_converter",
24
+ "html_to_markdown",
25
+ "timezone_lookup",
26
+ "xml_formatter"
27
+ ]
@@ -0,0 +1,206 @@
1
+ """
2
+ AWS S3 interaction functions
3
+ """
4
+ # import time
5
+ # import json
6
+ # import os
7
+ # import sys
8
+ # import urllib.request
9
+ # import resource # used to monitor memory
10
+ from loguru import logger
11
+
12
+ import boto3 # Library: boto3 -- for interacting with AWS S3 buckets
13
+ from botocore.exceptions import ClientError
14
+ from typing import Any
15
+
16
+ S3_BUCKETS = {}
17
+ S3_CLIENT: Any = None
18
+ S3_RESOURCE: Any = None
19
+
20
+ # =============================================================================
21
+ # --- INTERACT WITH AN S3 BUCKET ---
22
+ # =============================================================================
23
+ # - s3_connection (aws_region, aws_key_id, aws_key, OPTIONAL use_client) -> Boolean
24
+ # - s3_open_bucket (bucket_name, aws_region, aws_key_id, aws_key) -> Boolean
25
+ # - s3_chkdir (bucket_name, path) -> Boolean
26
+ # - s3_chkfile (bucket_name, path) -> Boolean
27
+ # - s3_mkdir (bucket_name, path) -> Boolean
28
+ # - s3_get_file (bucket_name, file_name) -> Boolean, String
29
+ # - s3_put_file (bucket_name, file_name, content)-> Boolean
30
+ # - s3_rm_file (bucket_name, file)
31
+ # =============================================================================
32
+
33
+ # Establishes a connection to the S3 service
34
+ # Returns True if successful (S3 service is available/reachable in the specified region and access key is accepted as valid).False otherwise.
35
+ def s3_connection(aws_region, aws_key_id, aws_key, use_client=False):
36
+ global S3_CLIENT, S3_RESOURCE
37
+ status = False
38
+ s3 = None
39
+ try:
40
+ if S3_CLIENT is None:
41
+ s3 = boto3.client(
42
+ service_name="s3",
43
+ region_name=aws_region,
44
+ aws_access_key_id=aws_key_id,
45
+ aws_secret_access_key=aws_key
46
+ )
47
+ S3_CLIENT = s3
48
+ else:
49
+ # s3 = S3_CLIENT # cache it for reuse
50
+ pass
51
+
52
+ if S3_RESOURCE is None:
53
+ s3 = boto3.resource(
54
+ service_name="s3",
55
+ region_name=aws_region,
56
+ aws_access_key_id=aws_key_id,
57
+ aws_secret_access_key=aws_key
58
+ )
59
+ S3_RESOURCE = s3 # cache it for reuse
60
+ else:
61
+ # s3 = S3_CLIENT # cache it for reuse
62
+ pass
63
+ status = True
64
+ except Exception as error:
65
+ logger.error(f"Unable to connect to AWS S3 service in {aws_region}. Possible invalid key or blocked communication. ({type(error).__name__}) {str(error)}")
66
+ return status
67
+
68
+ # Opens a connect to an S3 bucket
69
+ # Returns True if successful (bucket is available and access is granted).False otherwise.
70
+ # Once a bucket is open, its object is cached until the application halts
71
+ def s3_open_bucket(bucket_name, aws_region, aws_key_id, aws_key):
72
+ global S3_RESOURCE, S3_BUCKETS
73
+ status = False
74
+ if bucket_name not in S3_BUCKETS:
75
+ status = s3_connection(aws_region, aws_key_id, aws_key)
76
+ if status:
77
+ try:
78
+ status = False
79
+ if S3_RESOURCE is not None:
80
+ s3_bucket = S3_RESOURCE.Bucket(bucket_name)
81
+ status = True
82
+ S3_BUCKETS[bucket_name] = s3_bucket # Cache the Resource connection to the Bucket
83
+ except ClientError as error:
84
+ logger.warning(f"Unable to connect to {bucket_name}: {error}")
85
+ except Exception as error:
86
+ logger.error(f"{bucket_name} S3 bucket not found or no access. ({type(error).__name__}) {str(error)}") # .message)
87
+
88
+ return status
89
+
90
+ # Checks for the existence of a folder or file on an S3 bucket
91
+ # Returns True if the folder is found
92
+ # Returns False if the folder's existence could not be determined or
93
+ # if there was an error creating the folder.
94
+ def s3_chkdir(bucket_name, path):
95
+ global S3_BUCKETS
96
+ status = False
97
+ # status, s3 = s3_connection(aws_region, aws_key_id, aws_key, True)
98
+ if bucket_name in S3_BUCKETS:
99
+ try:
100
+ for obj in S3_BUCKETS[bucket_name].objects.all():
101
+ if path == obj.key or path + "/" == obj.key:
102
+ # logger.info(path + " found in S3 Bucket")
103
+ status = True
104
+ break
105
+ if not status:
106
+ # logger.info(path + " NOT found in S3 Bucket.")
107
+ pass
108
+ except Exception as error:
109
+ if type(error).__name__ == "RequestTimeTooSkewed":
110
+ logger.error("Local host's time is too far out of sync with AWS time. ** !! This is a common problem with WSL after the local host wakes from sleep. Fix time in the local time and try again.")
111
+ else:
112
+ logger.error(f"Error checking folder on S3 bucket. ({type(error).__name__}) {str(error)}")
113
+ else:
114
+ logger.error("Attempt to check directory on S3 bucket before opening S3 bucket.")
115
+
116
+ return status
117
+
118
+ # Creates a folder on an S3 bucket
119
+ # Returns True if the folder already exists or was created successfully
120
+ # Returns False if the folder's existence could not be determined or
121
+ # if there was an error creating the folder.
122
+ def s3_mkdir(bucket_name, path):
123
+ global S3_BUCKETS
124
+ status = False
125
+ # status, s3 = s3_connection(aws_region, aws_key_id, aws_key, True)
126
+ if bucket_name in S3_BUCKETS:
127
+ status = s3_chkdir(bucket_name, path)
128
+ if not status:
129
+ try:
130
+ S3_BUCKETS[bucket_name].put_object(Key=path + "/")
131
+ status = True
132
+ except Exception as error:
133
+ logger.error(f"Problem on S3 creating {path} ({type(error).__name__}) {str(error)}")
134
+ else:
135
+ logger.error("Attempt to make directory on S3 bucket before opening S3 bucket.")
136
+
137
+ return status
138
+
139
+ # Gets a file from a named S3 bucket
140
+ # Returns a boolean (true if successful, false if not) and actual file content
141
+ def s3_get_file(bucket_name, file_name):
142
+ global S3_CLIENT
143
+ status = False
144
+ ret_value = ""
145
+ if S3_CLIENT is not None:
146
+ try:
147
+ content_object = S3_CLIENT.get_object(Bucket=bucket_name, Key=file_name)
148
+ ret_value = content_object['Body'].read().decode('utf-8')
149
+ status = True
150
+ except ClientError as e:
151
+ ret_value = ""
152
+ error_code = e.response["Error"]["Code"]
153
+ if error_code == "AccessDenied":
154
+ logger.error("Access Denied fetching " + file_name)
155
+ else: # error_code == "InvalidLocationConstraint":
156
+ logger.error(f"{e.response['Error']['Code']} fetching {file_name}: {e.response['Error']['Message']}")
157
+ except Exception as error:
158
+ ret_value = ""
159
+ logger.error(f"Problem fetching {file_name} in S3 bucket {bucket_name} ({type(error).__name__}) {str(error)}")
160
+ else:
161
+ logger.error("Attempt to get file on S3 bucket before opening S3 bucket.")
162
+
163
+ return status, ret_value
164
+
165
+ # Saves a file to a named S3 bucket
166
+ # Returns true if successful, false if not
167
+ def s3_put_file(bucket_name, file_name, content):
168
+ global S3_CLIENT
169
+ status = False
170
+ if S3_CLIENT is not None:
171
+ try:
172
+ S3_CLIENT.put_object(Bucket=bucket_name, Key=file_name, Body=content, ContentEncoding="utf-8")
173
+ status = True
174
+ except ClientError as e:
175
+ # ['Error']['Code'] e.g. 'EntityAlreadyExists' or 'ValidationError'
176
+ # ['ResponseMetadata']['HTTPStatusCode'] e.g. 400
177
+ # ['ResponseMetadata']['RequestId'] e.g. 'd2b06652-88d7-11e5-99d0-812348583a35'
178
+ # ['Error']['Message'] e.g. "An error occurred (EntityAlreadyExists) ..."
179
+ # ['Error']['Type'] e.g. 'Sender'
180
+ error_code = e.response["Error"]["Code"]
181
+ if error_code == "AccessDenied":
182
+ logger.error("Access Denied saving " + file_name)
183
+ else: # error_code == "InvalidLocationConstraint":
184
+ logger.error(f"{e.response['Error']['Code']} saving {file_name}: {e.response['Error']['Message']}")
185
+ except Exception as error:
186
+ logger.error(f"Problem saving {file_name} in S3 bucket {bucket_name} ({type(error).__name__}) {str(error)}")
187
+ return status
188
+
189
+ # Removes a file from an S3 bucket
190
+ # Returns true if successful, false if not
191
+ def s3_rm_file(bucket_name, file):
192
+ logger.error("S3 Remove File not implemented (common.py/s3_rm_file)")
193
+ return False
194
+
195
+
196
+
197
+ # =============================================================================
198
+ # --- MAIN: Only runs if the module is executed stand-alone. ---
199
+ # =============================================================================
200
+ if __name__ == '__main__':
201
+ # Execute when the module is not initialized from an import statement.
202
+ logger.info("--- START ---")
203
+
204
+ logger.info("This contains functions common the OSCAL services capability. This module does nothing when run individually.")
205
+
206
+ logger.info("--- END ---")