strideutils 0.4.5__tar.gz → 0.5.0__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.
Files changed (34) hide show
  1. {strideutils-0.4.5 → strideutils-0.5.0}/PKG-INFO +12 -4
  2. {strideutils-0.4.5 → strideutils-0.5.0}/README.md +10 -3
  3. {strideutils-0.4.5 → strideutils-0.5.0}/pyproject.toml +2 -1
  4. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/coingecko.py +5 -11
  5. strideutils-0.5.0/strideutils/config_examples/env.local.example +24 -0
  6. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/exec_tx.py +4 -2
  7. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/slack_connector.py +21 -0
  8. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/stride_config.py +8 -13
  9. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/stride_requests.py +27 -11
  10. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/stride_upstash.py +47 -3
  11. strideutils-0.4.5/strideutils/config_examples/strideutils_secrets.yaml.example +0 -28
  12. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/Makefile +0 -0
  13. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/__init__.py +0 -0
  14. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/config_examples/strideutils_config.yaml.example +0 -0
  15. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/cryptography.py +0 -0
  16. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/invariant_helpers.py +0 -0
  17. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/run_safely.py +0 -0
  18. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/sheets_connector.py +0 -0
  19. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/stride_alerts.py +0 -0
  20. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/twilio_connector.py +0 -0
  21. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils/types.py +0 -0
  22. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/__init__.pyi +0 -0
  23. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/coingecko.pyi +0 -0
  24. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/cryptography.pyi +0 -0
  25. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/exec_tx.pyi +0 -0
  26. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/invariant_helpers.pyi +0 -0
  27. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/run_safely.pyi +0 -0
  28. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/sheets_connector.pyi +0 -0
  29. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/slack_connector.pyi +0 -0
  30. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/stride_alerts.pyi +0 -0
  31. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/stride_config.pyi +0 -0
  32. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/stride_requests.pyi +0 -0
  33. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/stride_upstash.pyi +0 -0
  34. {strideutils-0.4.5 → strideutils-0.5.0}/strideutils-stubs/twilio_connector.pyi +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: strideutils
3
- Version: 0.4.5
3
+ Version: 0.5.0
4
4
  Summary:
5
5
  Author: Your Name
6
6
  Author-email: you@example.com
@@ -18,6 +18,7 @@ Requires-Dist: gspread (>=6.0.2)
18
18
  Requires-Dist: pandas (>=2.1.1)
19
19
  Requires-Dist: pycryptodome (>=3.20.0)
20
20
  Requires-Dist: python-dateutil (>=2.8.2)
21
+ Requires-Dist: python-dotenv (==1.0.1)
21
22
  Requires-Dist: pytz (>=2023.3)
22
23
  Requires-Dist: pyyaml (>=6.0.1)
23
24
  Requires-Dist: redis (>=4.5.4)
@@ -49,12 +50,11 @@ Strideutils requires three different environment variables that can be added to
49
50
 
50
51
  ```
51
52
  export STRIDEUTILS_CONFIG_PATH=
52
- export STRIDEUTILS_SECRETS_PATH=
53
- export ENV=DEV
53
+ export STRIDEUTILS_ENV_PATH=
54
54
  ```
55
55
 
56
56
  Examples of these files are included under strideutils/config_examples.
57
- Stride Labs employees can find config.yaml in launchpad and strideutils_secrets.yamls in lastpass.
57
+ Stride Labs employees can find config.yaml in launchpad and .env.local in lastpass. We recommend you place .env.local in your launchpad repo.
58
58
 
59
59
  Any configuration or secrets that aren't consumed don't need to be set. However if one is accessed but unset, an error will be thrown for easier debugging.
60
60
 
@@ -77,6 +77,14 @@ import sys
77
77
  sys.path = ['/path/to/strideutils/'] + sys.path
78
78
  ```
79
79
 
80
+ Alternatively, you can install strideutils through local mode. However, please be warned that this will make your local Python environment _always_ use your local strideutils. This might lead to unexpected behavior if you local changes.
81
+
82
+ To install in local mode, run the following:
83
+
84
+ ```
85
+ pip3 install -e /Users/username/Documents/strideutils
86
+ ```
87
+
80
88
  Confirm the location of where it's being imported from by printing the module.
81
89
  After making changes to strideutils, reload it before testing your application.
82
90
 
@@ -20,12 +20,11 @@ Strideutils requires three different environment variables that can be added to
20
20
 
21
21
  ```
22
22
  export STRIDEUTILS_CONFIG_PATH=
23
- export STRIDEUTILS_SECRETS_PATH=
24
- export ENV=DEV
23
+ export STRIDEUTILS_ENV_PATH=
25
24
  ```
26
25
 
27
26
  Examples of these files are included under strideutils/config_examples.
28
- Stride Labs employees can find config.yaml in launchpad and strideutils_secrets.yamls in lastpass.
27
+ Stride Labs employees can find config.yaml in launchpad and .env.local in lastpass. We recommend you place .env.local in your launchpad repo.
29
28
 
30
29
  Any configuration or secrets that aren't consumed don't need to be set. However if one is accessed but unset, an error will be thrown for easier debugging.
31
30
 
@@ -48,6 +47,14 @@ import sys
48
47
  sys.path = ['/path/to/strideutils/'] + sys.path
49
48
  ```
50
49
 
50
+ Alternatively, you can install strideutils through local mode. However, please be warned that this will make your local Python environment _always_ use your local strideutils. This might lead to unexpected behavior if you local changes.
51
+
52
+ To install in local mode, run the following:
53
+
54
+ ```
55
+ pip3 install -e /Users/username/Documents/strideutils
56
+ ```
57
+
51
58
  Confirm the location of where it's being imported from by printing the module.
52
59
  After making changes to strideutils, reload it before testing your application.
53
60
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "strideutils"
3
- version = "0.4.5"
3
+ version = "0.5.0"
4
4
  description = ""
5
5
  authors = ["Your Name <you@example.com>"]
6
6
  readme = "README.md"
@@ -21,6 +21,7 @@ slack-sdk = ">=3.23.0"
21
21
  pandas = ">=2.1.1"
22
22
  twilio = ">=8.10.0"
23
23
  tabulate = ">=0.9.0"
24
+ python-dotenv = "==1.0.1"
24
25
  pyyaml = ">=6.0.1"
25
26
  python-dateutil = ">=2.8.2"
26
27
  pycryptodome = ">=3.20.0"
@@ -79,11 +79,8 @@ def get_token_price_history(token: str, num_days=90, use_key=False, memo={}) ->
79
79
  price_key = (token, num_days)
80
80
  if price_key not in memo:
81
81
  tstr = get_coingecko_name(config.get_chain(ticker=token))
82
- rurl = (
83
- f"https://api.coingecko.com/api/v3/coins/{tstr}/market_chart?vs_currency=usd&days={num_days}" # noqa:E231
84
- )
82
+ rurl = f"{COINGECKO_ENDPOINT}api/v3/coins/{tstr}/market_chart?vs_currency=usd&days={num_days}" # noqa:E231
85
83
  rel_headers = {"x-cg-pro-api-key": config.COINGECKO_API_TOKEN} if use_key else None
86
-
87
84
  r = requests.get(
88
85
  rurl,
89
86
  headers=rel_headers,
@@ -98,7 +95,7 @@ def get_token_price_history(token: str, num_days=90, use_key=False, memo={}) ->
98
95
  return memo[token]
99
96
 
100
97
 
101
- def get_token_volume_history(token: str, num_days=90, use_key=False, memo={}) -> pd.Series:
98
+ def get_token_volume_history(token: str, num_days=90, use_key=True, memo={}) -> pd.Series:
102
99
  """
103
100
  returns a Pandas Series with token volume data going back num_days
104
101
  e.g. get_token_volume_history('uatom', 30) returns a Series with 30 days of ATOM volume data
@@ -107,11 +104,8 @@ def get_token_volume_history(token: str, num_days=90, use_key=False, memo={}) ->
107
104
  volume_key = (token, num_days)
108
105
  if volume_key not in memo:
109
106
  tstr = get_coingecko_name(config.get_chain(ticker=token))
110
- rurl = (
111
- f"https://api.coingecko.com/api/v3/coins/{tstr}/market_chart?vs_currency=usd&days={num_days}" # noqa:E231
112
- )
107
+ rurl = f"{COINGECKO_ENDPOINT}api/v3/coins/{tstr}/market_chart?vs_currency=usd&days={num_days}" # noqa:E231
113
108
  rel_headers = {"x-cg-pro-api-key": config.COINGECKO_API_TOKEN} if use_key else None
114
-
115
109
  r = requests.get(
116
110
  rurl,
117
111
  headers=rel_headers,
@@ -126,7 +120,7 @@ def get_token_volume_history(token: str, num_days=90, use_key=False, memo={}) ->
126
120
  return memo[token]
127
121
 
128
122
 
129
- def get_dataframe_of_prices(token_list: List[str], num_days: int = 90) -> pd.DataFrame:
123
+ def get_dataframe_of_prices(token_list: List[str], num_days: int = 90, use_key: bool = True) -> pd.DataFrame:
130
124
  """
131
125
  returns a Pandas DataFrame with token price data going back num_days, for the given tokens
132
126
  e.g. get_dataframe_of_prices(["ATOM", "OSMO", "STRD"], 30) returns a DataFrame of 30 days of price data
@@ -134,7 +128,7 @@ def get_dataframe_of_prices(token_list: List[str], num_days: int = 90) -> pd.Dat
134
128
  # gather the dataframe
135
129
  df = {}
136
130
  for token in token_list:
137
- df[token] = get_token_price_history(token, num_days)["price"]
131
+ df[token] = get_token_price_history(token, num_days, use_key=use_key)["price"]
138
132
  df = pd.DataFrame(df)
139
133
  # resample based on how much data we're gathering
140
134
  if num_days > 90:
@@ -0,0 +1,24 @@
1
+ ENV=DEV
2
+ CONFIGURE_STRIDEUTILS_LOGS=true
3
+
4
+ # Slack
5
+ SLACK_CHANNELS={"test": "U33D4351C2V"}
6
+
7
+ # API Tokens
8
+ NUMIA_API_TOKEN=token
9
+ COINGECKO_API_TOKEN=cgtoken
10
+
11
+ # Upstash
12
+ UPSTASH_PUBLIC_PASSWORD=password
13
+
14
+ # Twilio
15
+ TWILIO_ALERTS_NUMBER=+11111111111
16
+
17
+ # Slashbot twitter
18
+ TWITTER_CONSUMER_KEY=consumerkey
19
+
20
+ # Epoch Summary
21
+ EPOCH_SUMMARY_SLACK_BEARER_TOKEN=token
22
+
23
+ # Google Sheets
24
+ PUBLICSHEETS_AUTH={}
@@ -18,14 +18,16 @@ class TxResponse:
18
18
  raw_log: Optional[str] = None
19
19
 
20
20
 
21
- def execute_tx(command: str, api_endpoint: str = config.stride.api_endpoint) -> TxResponse:
21
+ def execute_tx(command: str, api_endpoint: str = config.stride.api_endpoint, verbose=False) -> TxResponse:
22
22
  """
23
23
  Execute a tx and query the tx hash
24
24
  """
25
25
  output = subprocess.run(command, shell=True, capture_output=True)
26
26
  standard_out = re.sub(r'\x1b\[[0-9;]*[mGKH]', '', output.stdout.decode('utf-8'))
27
27
  error_out = re.sub(r'\x1b\[[0-9;]*[mGKH]', '', output.stderr.decode('utf-8'))
28
-
28
+ if verbose:
29
+ print("Standard Out: ", standard_out)
30
+ print("Error Out: ", error_out)
29
31
  tx_hash = ""
30
32
  for output_line in standard_out.split('\n'):
31
33
  if 'txhash' in output_line:
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import traceback
2
3
  from typing import List, Optional, Union
3
4
 
4
5
  from slack_sdk import WebClient
@@ -51,3 +52,23 @@ def post_msg(
51
52
  print("Error sending message: ", e)
52
53
  raise
53
54
  return thread_ts
55
+
56
+
57
+ def upload_file(file_name: str, content: str) -> str:
58
+ '''
59
+ This uploads a file called "file_name" with the string contents "content"
60
+
61
+ This will return a link that can be used to embed a file in slack.
62
+ For example:
63
+ url = upload_file("test.txt", "Hello World")
64
+ post_msg(f"<{url}|This is a file>", channel="#alerts-debug")
65
+ '''
66
+ client = WebClient(token=config.STRIDEBOT_API_TOKEN)
67
+ try:
68
+ slack_file = client.files_upload_v2(filename=file_name, content=content)
69
+ except Exception as e:
70
+ print(f"Error uploading file to slack: {e}")
71
+ traceback.print_exc()
72
+ raise ValueError(f"Error uploading file to slack: {e}")
73
+ file_link = slack_file['file']['permalink']
74
+ return file_link
@@ -8,6 +8,7 @@ from dataclasses import dataclass, field, fields
8
8
  from typing import Any, List, Optional, Union
9
9
 
10
10
  import yaml
11
+ from dotenv import load_dotenv
11
12
 
12
13
  MISSING_CONFIG_MSG = "Configuration or environment variable '{x}' is not set or unknown"
13
14
 
@@ -191,6 +192,8 @@ class Config(ConfigObj):
191
192
  UPSTASH_STRIDE_FRONTEND_PASSWORD: str = ""
192
193
  UPSTASH_STRIDE_BACKEND_PASSWORD: str = ""
193
194
  UPSTASH_STRIDE_DYDX_PUBLIC_PASSWORD: str = ""
195
+ UPSTASH_STRIDE_DYDX_AIRDROP_PASSWORD: str = ""
196
+ UPSTASH_STRIDE_MILESTONES_PASSWORD: str = ""
194
197
 
195
198
  # Stride internal secrets
196
199
  SLACK_BEARER_TOKEN: str = ""
@@ -317,7 +320,6 @@ def _load_raw_config() -> dict[str, Any]:
317
320
  'Shell var STRIDEUTILS_CONFIG_PATH is missing or the file does not exist in the default location.'
318
321
  'The default file and location is config/config.yaml in the current working directory'
319
322
  )
320
-
321
323
  with open(strideutils_config_path, 'r') as config_file:
322
324
  raw_config = yaml.load(config_file, Loader)
323
325
 
@@ -327,20 +329,13 @@ def _load_raw_config() -> dict[str, Any]:
327
329
  def _load_raw_secrets() -> dict[str, Optional[str]]:
328
330
  """
329
331
  Loads the secret variables into a dictionary
330
- If this is running locally in dev mode, secrets are grabbed from the path defined by STRIDEUTILS_SECRETS_PATH
332
+ If this is running locally in dev mode, secrets are grabbed from the path defined by STRIDEUTILS_ENV_PATH
331
333
  Otherwise, their loaded as environment variables
332
334
  """
333
- if ENV == "PROD":
334
- return {secret.name: os.environ.get(secret.name) for secret in fields(Config) if os.environ.get(secret.name)}
335
-
336
- if not (strideutils_secrets_path := os.environ.get('STRIDEUTILS_SECRETS_PATH')):
337
- raise ValueError(MISSING_CONFIG_MSG.format(x='STRIDEUTILS_SECRETS_PATH'))
338
-
339
- with open(strideutils_secrets_path, 'r') as f:
340
- raw_secrets = yaml.load(f, Loader)
341
- raw_secrets.update({'ENV': ENV})
342
-
343
- return raw_secrets
335
+ strideutils_env_path = os.environ.get('STRIDEUTILS_ENV_PATH', '.env.local')
336
+ if os.path.exists(strideutils_env_path):
337
+ load_dotenv(strideutils_env_path)
338
+ return {secret.name: os.environ.get(secret.name) for secret in fields(Config) if os.environ.get(secret.name)}
344
339
 
345
340
 
346
341
  def _parse_raw_configs(raw_config: dict[str, Any], raw_secrets: dict[str, Optional[str]]) -> Config:
@@ -973,18 +973,20 @@ def get_delegations(delegator_address: str, api_endpoint: str = config.stride.ap
973
973
  return delegations_by_validator
974
974
 
975
975
 
976
- def get_host_zone_delegations(host_zone_id: str, include_ground_truth: bool = True) -> pd.DataFrame:
976
+ def _parse_validator_dict(raw_validators: List[dict[str, Any]]) -> pd.DataFrame:
977
977
  """
978
- Given a host zone id, returns a Dataframe of validator delegations.
978
+ Given the output of a host_zone's "validator" field,
979
+ this will parse the data into a Pandas DataFrame.
979
980
 
980
- If `include_ground_truth=True`, then will also return the "true" delegations
981
- on the host zone delegaiton account.
981
+ Will also cast the relevant columns to ints and floats.
982
982
  """
983
- int_cols = [
983
+ int_fields = [
984
984
  "weight",
985
985
  "delegation",
986
- "slash_query_progress_tracker",
987
986
  "slash_query_checkpoint",
987
+ "slash_query_progress_tracker",
988
+ ]
989
+ float_cols = [
988
990
  "shares_to_tokens_rate",
989
991
  ]
990
992
  output_cols = [
@@ -997,12 +999,26 @@ def get_host_zone_delegations(host_zone_id: str, include_ground_truth: bool = Tr
997
999
  "shares_to_tokens_rate",
998
1000
  ]
999
1001
 
1000
- # Query the validator delegations on the host zone
1001
- host_zone = get_host_zone(host_zone_id)
1002
- validator_df = pd.DataFrame(host_zone["validators"])
1003
- validator_df[int_cols] = validator_df[int_cols].map(lambda i: int(float(i))) # type: ignore
1004
- validator_df = validator_df[output_cols]
1002
+ def parse_validator(field_name: str, field_value: Any) -> Any:
1003
+ if field_name in int_fields:
1004
+ return int(field_value)
1005
+ if field_name in float_cols:
1006
+ return float(field_value)
1007
+ return field_value
1008
+
1009
+ cast_validators = [{k: parse_validator(k, v) for k, v in val_dict.items()} for val_dict in raw_validators]
1010
+ return pd.DataFrame(cast_validators, dtype=object)[output_cols]
1005
1011
 
1012
+
1013
+ def get_host_zone_delegations(host_zone_id: str, include_ground_truth: bool = True) -> pd.DataFrame:
1014
+ """
1015
+ Given a host zone id, returns a Dataframe of validator delegations.
1016
+
1017
+ If `include_ground_truth=True`, then will also return the "true" delegations
1018
+ on the host zone delegaiton account.
1019
+ """
1020
+ host_zone = get_host_zone(host_zone_id)
1021
+ validator_df = _parse_validator_dict(host_zone["validators"])
1006
1022
  # If the ground truth isn't requested, return as is
1007
1023
  if not include_ground_truth:
1008
1024
  return validator_df
@@ -1,4 +1,4 @@
1
- from typing import Dict, Optional
1
+ from typing import Dict, List, Optional
2
2
 
3
3
  import redis
4
4
 
@@ -43,7 +43,20 @@ def get_redis_db(redis_db_name: str) -> redis.Redis:
43
43
  password=config.UPSTASH_STRIDE_DYDX_PUBLIC_PASSWORD,
44
44
  ssl=True,
45
45
  )
46
-
46
+ elif redis_db_name == 'dydx_airdrop':
47
+ r = redis.Redis(
48
+ host='pleasant-pipefish-54904.upstash.io',
49
+ port=6379,
50
+ password=config.UPSTASH_STRIDE_DYDX_AIRDROP_PASSWORD,
51
+ ssl=True,
52
+ )
53
+ elif redis_db_name == "milestones":
54
+ r = redis.Redis(
55
+ host='adapting-snail-56076.upstash.io',
56
+ port=6379,
57
+ password=config.UPSTASH_STRIDE_MILESTONES_PASSWORD,
58
+ ssl=True,
59
+ )
47
60
  else:
48
61
  raise ValueError(f'Invalid Redis DB: {redis_db_name}')
49
62
 
@@ -64,6 +77,35 @@ def get(redis_key: str, db_name='frontend') -> Optional[str]:
64
77
  return value.decode('utf-8') if value is not None else None
65
78
 
66
79
 
80
+ def get_multiple_keys(redis_keys: List[str], db_name='frontend') -> List[Optional[str]]:
81
+ """
82
+ This function will read multiple keys in at once.
83
+ Value will be "None" if the key does not exist.
84
+
85
+ Pulls from the specified redis db.
86
+ """
87
+ db = get_redis_db(db_name)
88
+ values = db.mget(redis_keys)
89
+
90
+ return [value.decode('utf-8') if value is not None else None for value in values]
91
+
92
+
93
+ def get_all_keys(db_name='dydx_airdrop') -> List[str]:
94
+ """
95
+ This function will return all keys in the redis db specified.
96
+ """
97
+ db = get_redis_db(db_name)
98
+
99
+ out = []
100
+
101
+ cursor = '0'
102
+ while cursor != 0:
103
+ cursor, keys = db.scan(cursor, count=1000)
104
+ if keys:
105
+ out.extend(keys)
106
+ return [key.decode('utf-8') for key in out]
107
+
108
+
67
109
  def set(redis_key: str, redis_val: str, db_name='frontend'):
68
110
  """
69
111
  This function will set the given key to value in the redis_db specified.
@@ -79,5 +121,7 @@ def set_keys(dict_to_upload: Dict[str, str], db_name='frontend', prefix=''):
79
121
  Will append "prefix" to all keys in the dict.
80
122
  """
81
123
  db = get_redis_db(db_name)
124
+ pipe = db.pipeline()
82
125
  for k, v in dict_to_upload.items():
83
- db.set(prefix + k, v)
126
+ pipe.set(prefix + k, v)
127
+ pipe.execute()
@@ -1,28 +0,0 @@
1
- # Twilio
2
- TWILIO_ALERTS_NUMBER: +18663326515
3
- TWILIO_ACCOUNT_ID:
4
- TWILIO_API_TOKEN:
5
- PHONE_NUMBERS: {}
6
-
7
- # Slack
8
- STRIDEBOT_API_TOKEN:
9
-
10
- # GSHEETS
11
- PUBLICSHEETS_AUTH:
12
-
13
- # Coingecko
14
- COINGECKO_API_TOKEN:
15
-
16
- # Stride Internal secrets
17
- SLACK_BEARER_TOKEN:
18
- SLACK_SUCCESS_CHANNEL_ID:
19
- SLACK_INVARIANT_FAILURE_CHANNEL_ID:
20
- SLACK_PACKET_FAILURE_CHANNEL_ID:
21
- NUMIA_API_TOKEN:
22
- UPSTASH_PUBLIC_PASSWORD:
23
- UPSTASH_STRIDE_FRONTEND_PASSWORD:
24
- UPSTASH_STRIDE_BACKEND_PASSWORD:
25
-
26
- # AWS
27
- AWS_ACCESS_KEY_ID:
28
- AWS_SECRET_ACCESS_KEY: