mind-castle 0.1.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,11 @@
1
+ Metadata-Version: 2.1
2
+ Name: mind-castle
3
+ Version: 0.1.1
4
+ Summary: A universal secrets manager
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: boto3>=1.35.38
8
+ Requires-Dist: sqlalchemy<3.0.0,>=2.0.0
9
+
10
+ # mind-castle
11
+ A universal secret store
@@ -0,0 +1,2 @@
1
+ # mind-castle
2
+ A universal secret store
File without changes
@@ -0,0 +1,20 @@
1
+ from typing import Optional
2
+ import uuid
3
+
4
+
5
+ class SecretStore():
6
+ '''This is an abstract class that defines the interface for a secret store.'''
7
+
8
+ def get_secret(self, key: str, default: str = None) -> Optional[str]:
9
+ raise NotImplementedError()
10
+
11
+ def put_secret(self, value: str) -> dict:
12
+ raise NotImplementedError()
13
+
14
+ def get_secret_key(self) -> str:
15
+ '''
16
+ Generates a key based on this machine's hardware and the time.
17
+ https://docs.python.org/3/library/uuid.html#uuid.uuid1
18
+ '''
19
+
20
+ return str(uuid.uuid1())
@@ -0,0 +1,122 @@
1
+ import json
2
+ import os
3
+ import boto3
4
+ from enum import Enum
5
+ from .secret_store import SecretStore
6
+ from typing import Optional
7
+
8
+
9
+ # If you add a secret store implementation here, make sure to add it to
10
+ # the SecretStoreType enum and TYPE_STORE_MAP dictionary at the bottom of this file
11
+
12
+
13
+ class MemorySecretStore(SecretStore):
14
+ '''
15
+ An in-memory secret store.
16
+ This is not persistent and will be lost when the application is restarted.
17
+ Use only for testing or development.
18
+ '''
19
+
20
+ store_type = "mem"
21
+
22
+ def __init__(self):
23
+ self.secrets = {}
24
+
25
+ def get_secret(self, key: str, default: str = None) -> Optional[str]:
26
+ return self.secrets.get(key, default)
27
+
28
+ def put_secret(self, value: str) -> dict:
29
+ key = self.get_secret_key()
30
+ self.secrets[key] = value
31
+ return {"secret_type": self.store_type, "key": key}
32
+
33
+
34
+ class JsonSecretStore(SecretStore):
35
+ store_type = "json"
36
+ filename = "secrets.json"
37
+
38
+ def __init__(self):
39
+ if not os.path.exists(self.filename):
40
+ with open(self.filename, "w") as f:
41
+ f.write("{}")
42
+
43
+ def get_secret(self, key: str, default: str = None) -> Optional[str]:
44
+ with open(self.filename, "r") as f:
45
+ secrets = json.load(f)
46
+ return secrets.get(key, default)
47
+
48
+ def put_secret(self, value: str) -> dict:
49
+ key = self.get_secret_key()
50
+ with open(self.filename, "r") as f:
51
+ secrets = json.load(f)
52
+ secrets[key] = value
53
+
54
+ with open(self.filename, "w") as f:
55
+ json.dump(secrets, f)
56
+ return {"secret_type": self.store_type, "key": key}
57
+
58
+
59
+ class AWSSecretsManagerSecretStore(SecretStore):
60
+ '''
61
+ Uses AWS Secrets Manager to store secrets.
62
+ This assumes you have credentials in the environment or in the default AWS credentials file.
63
+ '''
64
+ store_type = "awssecretsmanager"
65
+
66
+ def __init__(self):
67
+ # TODO: Use this region param as a test for how to pass args to a secret store init
68
+ self.client = boto3.client('secretsmanager', region_name='us-east-2')
69
+
70
+ def get_secret(self, key: str, default: str = None) -> Optional[str]:
71
+ response = self.client.get_secret_value(SecretId=key)
72
+ return response.get("SecretString", default)
73
+
74
+ def put_secret(self, value: str) -> dict:
75
+ key = self.get_secret_key()
76
+ # Response is always a success. This throws an exception if it fails
77
+ response = self.client.create_secret(
78
+ Name=key,
79
+ ClientRequestToken=key,
80
+ # Description='string',
81
+ # KmsKeyId='string',
82
+ # SecretBinary=b'bytes',
83
+ SecretString=value,
84
+ # Tags=[
85
+ # {
86
+ # 'Key': 'string',
87
+ # 'Value': 'string'
88
+ # },
89
+ # ],
90
+ # AddReplicaRegions=[
91
+ # {
92
+ # 'Region': 'string',
93
+ # 'KmsKeyId': 'string'
94
+ # },
95
+ # ],
96
+ # ForceOverwriteReplicaSecret=True|False
97
+ )
98
+ return {"secret_type": self.store_type, "key": key, "arn": response.get("ARN")}
99
+
100
+
101
+ class SecretStoreType(str, Enum):
102
+ '''List of supported secret store types'''
103
+ MEMORY = MemorySecretStore.store_type
104
+ JSON = JsonSecretStore.store_type
105
+ AWS_SECRETS_MANAGER = AWSSecretsManagerSecretStore.store_type
106
+
107
+
108
+ # Map of secret store types to their implementations
109
+ TYPE_STORE_MAP = {
110
+ SecretStoreType.MEMORY: MemorySecretStore,
111
+ SecretStoreType.JSON: JsonSecretStore,
112
+ SecretStoreType.AWS_SECRETS_MANAGER: AWSSecretsManagerSecretStore
113
+ }
114
+
115
+
116
+ def get_secret_store(secret_type: SecretStoreType) -> SecretStore:
117
+ '''Instantiates and returns a SecretStore implementation with the given type.'''
118
+
119
+ store_class = TYPE_STORE_MAP.get(secret_type)
120
+ if store_class is None:
121
+ raise ValueError(f"Unknown secret store type '{secret_type}'")
122
+ return store_class()
@@ -0,0 +1,50 @@
1
+ import logging
2
+ import sqlalchemy.types as types
3
+ import json
4
+
5
+ from .secret_stores import get_secret_store, SecretStoreType
6
+
7
+
8
+ logger = logging.getLogger(__name__)
9
+ logger.level = logging.DEBUG
10
+
11
+
12
+ class SecretData(types.TypeDecorator):
13
+ '''A sqlalchemy field type that stores data in a secret store.'''
14
+
15
+ impl = types.JSON # The base data type for this field
16
+ cache_ok = True
17
+
18
+ def __init__(self, store_type: SecretStoreType, *args, **kwargs):
19
+ super().__init__(*args, **kwargs)
20
+ # Get an implementation from the store factory
21
+ # based on the given store_type
22
+ self.secret_type = store_type
23
+ self.secret_store = get_secret_store(store_type)
24
+
25
+ def process_bind_param(self, value, dialect):
26
+ logger.debug(f"Storing {value} of type {type(value)} as a secret.")
27
+ # This might not be a dict or list, but let's json dump everything to be consistent
28
+ stringValue = json.dumps(value, sort_keys=True)
29
+
30
+ store_params = self.secret_store.put_secret(stringValue)
31
+ logger.debug(f"Stored {stringValue} as {store_params}")
32
+ return store_params
33
+
34
+ def process_result_value(self, value, dialect):
35
+ if value == {}:
36
+ return {}
37
+ logger.debug(f"Restoring {value} from a secret.")
38
+ secret_type = value.get("secret_type")
39
+ key = value.get("key")
40
+ if secret_type is None:
41
+ raise ValueError(f"Secret data is missing secret_type: {value}")
42
+
43
+ # We can't assume the secret type here is the same as 'self.secret_type'
44
+ # because the user may have been using a different secret store type previously
45
+ store = get_secret_store(secret_type) if secret_type != self.secret_type else self.secret_store
46
+ result = store.get_secret(key)
47
+ logger.debug(f"Restored {result} from key {key}")
48
+ if result is None:
49
+ return {}
50
+ return json.loads(result)
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.1
2
+ Name: mind-castle
3
+ Version: 0.1.1
4
+ Summary: A universal secrets manager
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: boto3>=1.35.38
8
+ Requires-Dist: sqlalchemy<3.0.0,>=2.0.0
9
+
10
+ # mind-castle
11
+ A universal secret store
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ mind_castle/__init__.py
4
+ mind_castle/secret_store.py
5
+ mind_castle/secret_stores.py
6
+ mind_castle/secret_type.py
7
+ mind_castle.egg-info/PKG-INFO
8
+ mind_castle.egg-info/SOURCES.txt
9
+ mind_castle.egg-info/dependency_links.txt
10
+ mind_castle.egg-info/requires.txt
11
+ mind_castle.egg-info/top_level.txt
12
+ tests/test_memory.py
@@ -0,0 +1,2 @@
1
+ boto3>=1.35.38
2
+ sqlalchemy<3.0.0,>=2.0.0
@@ -0,0 +1 @@
1
+ mind_castle
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "mind-castle"
3
+ version = "0.1.1"
4
+ description = "A universal secrets manager"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "boto3>=1.35.38",
9
+ "sqlalchemy>=2.0.0,<3.0.0",
10
+ ]
11
+
12
+ [tool.pytest.ini_options]
13
+ pythonpath = [
14
+ "."
15
+ ]
16
+
17
+ [tool.uv]
18
+ dev-dependencies = [
19
+ "pytest>=8.3.3",
20
+ ]
21
+ package = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,15 @@
1
+ from memory_secret_store import MemorySecretStore
2
+
3
+
4
+ def test_put_secret():
5
+ secret_store = MemorySecretStore()
6
+ secret_store.put_secret("some_secret_key", "some_secret_value")
7
+
8
+ assert secret_store.secrets["some_secret_key"] == "some_secret_value"
9
+
10
+
11
+ def test_get_secret():
12
+ secret_store = MemorySecretStore()
13
+ secret_store.put_secret("some_secret_key", "some_secret_value")
14
+
15
+ assert secret_store.get_secret("some_secret_key") == "some_secret_value"