File size: 9,941 Bytes
e00b837
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Hive Appier Framework
# Copyright (c) 2008-2024 Hive Solutions Lda.
#
# This file is part of Hive Appier Framework.
#
# Hive Appier Framework is free software: you can redistribute it and/or modify
# it under the terms of the Apache License as published by the Apache
# Foundation, either version 2.0 of the License, or (at your option) any
# later version.
#
# Hive Appier Framework is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# Apache License for more details.
#
# You should have received a copy of the Apache License along with
# Hive Appier Framework. If not, see <http://www.apache.org/licenses/>.

__author__ = "João Magalhães <joamag@hive.pt>"
""" The author(s) of the module """

__copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda."
""" The copyright for the module """

__license__ = "Apache License, Version 2.0"
""" The license for the module """

import os
import sys
import json

from . import legacy

FILE_NAME = "appier.json"
""" The default name of the file that is going to be
used for the loading of configuration values from JSON """

FILE_TEMPLATE = "appier.%s.json"
""" The template to be used in the construction of the
domain specific configuration file paths """

HOME_FILE = "~/.home"
""" The location of the file that may be used to "redirect"
the home directory contents to a different directory """

IMPORT_NAMES = ("$import", "$include", "$IMPORT", "$INCLUDE")
""" The multiple possible definitions of the special configuration
name that references a list of include files to be loaded """

CASTS = {
    bool: lambda v: v if isinstance(v, bool) else v in ("1", "true", "True"),
    list: lambda v: v if isinstance(v, list) else v.split(";") if v else [],
    tuple: lambda v: v if isinstance(v, tuple) else tuple(v.split(";") if v else []),
}
""" The map containing the various cast method
operation associated with the various data types,
they provide a different type of casting strategy """

ENV_ENCODINGS = ("utf-8", sys.getdefaultencoding(), sys.getfilesystemencoding())
""" The sequence of encodings that are going to
be used to try to decode possible byte based strings
for the various environment variable values """

CONFIGS = {}
""" The map that contains the key value association
for all the currently set global configurations """

CONFIG_F = []
""" The list of files that have been used for the loading
of the configuration through this session, every time a
loading of configuration from a file occurs the same path
is added to this global list """

HOMES = []
""" Global reference to the paths to the directory considered
to be the home on in terms of configuration, this value should
be set on the initial loading of the ".home" file """

if not isinstance(__builtins__, dict):
    __builtins__ = __builtins__.__dict__


def conf(name, default=None, cast=None, ctx=None):
    """
    Retrieves the configuration value for the provided value
    defaulting to the provided default value in case no value
    is found for the provided name.

    An optional cast operation may be performed on the value
    in case it's requested.

    :type name: String
    :param name: The name of the configuration value to be
    retrieved.
    :type default: Object
    :param default: The default value to be retrieved in case
    no value was found for the provided name.
    :type cast: Type/String
    :param cast: The cast operation to be performed in the
    resolved value (optional).
    :type ctx: Dictionary
    :param ctx: The context dictionary to be used for situations
    where a more contextual configuration is meant to be used instead
    of the process wide global configuration.
    :rtype: Object
    :return: The value for the configuration with the requested
    name or the default value if no value was found.
    """

    configs = ctx["configs"] if ctx else CONFIGS
    cast = _cast_r(cast)
    value = configs.get(name, default)
    if cast and not value == None:
        value = cast(value)
    return value


def conf_prefix(prefix, ctx=None):
    configs = ctx["configs"] if ctx else CONFIGS
    configs_prefix = dict()
    for name, value in configs.items():
        if not name.startswith(prefix):
            continue
        configs_prefix[name] = value
    return configs_prefix


def conf_suffix(suffix, ctx=None):
    configs = ctx["configs"] if ctx else CONFIGS
    configs_suffix = dict()
    for name, value in configs.items():
        if not name.endswith(suffix):
            continue
        configs_suffix[name] = value
    return configs_suffix


def conf_s(name, value, ctx=None):
    configs = ctx["configs"] if ctx else CONFIGS
    configs[name] = value


def conf_r(name, ctx=None):
    configs = ctx["configs"] if ctx else CONFIGS
    if not name in configs:
        return
    del configs[name]


def conf_d(ctx=None):
    configs = ctx["configs"] if ctx else CONFIGS
    return configs


def conf_ctx():
    return dict(configs=dict(), config_f=dict())


def load(names=(FILE_NAME,), path=None, encoding="utf-8", ctx=None):
    paths = []
    homes = get_homes()
    for home in homes:
        paths += [
            os.path.join(home),
            os.path.join(home, ".config"),
        ]
    paths += [sys.prefix]
    paths.append(path)
    for path in paths:
        for name in names:
            load_file(name=name, path=path, encoding=encoding, ctx=ctx)
    load_env(ctx=ctx)


def load_file(name=FILE_NAME, path=None, encoding="utf-8", ctx=None):
    configs = ctx["configs"] if ctx else CONFIGS
    config_f = ctx["config_f"] if ctx else CONFIG_F

    if path:
        path = os.path.normpath(path)
    if path:
        file_path = os.path.join(path, name)
    else:
        file_path = name

    file_path = os.path.abspath(file_path)
    file_path = os.path.normpath(file_path)
    base_path = os.path.dirname(file_path)

    exists = os.path.exists(file_path)
    if not exists:
        return

    exists = file_path in config_f
    if exists:
        config_f.remove(file_path)
    config_f.append(file_path)

    file = open(file_path, "rb")
    try:
        data = file.read()
    finally:
        file.close()
    if not data:
        return

    data = data.decode(encoding)
    data_j = json.loads(data)

    _load_includes(base_path, data_j, encoding=encoding)

    for key, value in data_j.items():
        if not _is_valid(key):
            continue
        configs[key] = value


def load_env(ctx=None):
    configs = ctx["configs"] if ctx else CONFIGS

    config = dict(os.environ)
    homes = get_homes()

    for home in homes:
        _load_includes(home, config)

    for key, value in legacy.iteritems(config):
        if not _is_valid(key):
            continue
        configs[key] = value
        is_bytes = legacy.is_bytes(value)
        if not is_bytes:
            continue
        for encoding in ENV_ENCODINGS:
            try:
                value = value.decode(encoding)
            except UnicodeDecodeError:
                pass
            else:
                break
        configs[key] = value


def get_homes(file_path=HOME_FILE, default="~", encoding="utf-8", force_default=False):
    global HOMES
    if HOMES:
        return HOMES

    HOMES = os.environ.get("HOMES", None)
    HOMES = HOMES.split(";") if HOMES else HOMES
    if not HOMES == None:
        return HOMES

    default = os.path.expanduser(default)
    default = os.path.abspath(default)
    default = os.path.normpath(default)
    HOMES = [default]

    file_path = os.path.expanduser(file_path)
    file_path = os.path.normpath(file_path)
    exists = os.path.exists(file_path)
    if not exists:
        return HOMES

    if not force_default:
        del HOMES[:]

    file = open(file_path, "rb")
    try:
        data = file.read()
    finally:
        file.close()

    data = data.decode("utf-8")
    data = data.strip()
    paths = data.splitlines()
    paths = [path.strip() for path in paths]

    for path in paths:
        path = path.strip()
        if not path:
            continue
        path = os.path.expanduser(path)
        path = os.path.abspath(path)
        path = os.path.normpath(path)
        HOMES.append(path)

    return HOMES


def _cast_r(cast):
    is_string = type(cast) in legacy.STRINGS
    if is_string:
        cast = __builtins__.get(cast, None)
    if not cast:
        return None
    return CASTS.get(cast, cast)


def _load_includes(base_path, config, encoding="utf-8"):
    includes = ()

    for alias in IMPORT_NAMES:
        includes = config.get(alias, includes)

    if legacy.is_string(includes):
        includes = includes.split(";")

    for include in includes:
        load_file(name=include, path=base_path, encoding=encoding)


def _is_valid(key):
    if key in IMPORT_NAMES:
        return False
    return True


def _is_devel():
    """
    Simple debug/development level detection mechanism to be
    used at load time to determine if the system is running
    under a development (debug) environment.

    This function should not be used at runtime as there are
    other (more powerful) mechanisms to archive the same
    type of results.

    :rtype: bool
    :return: If the current environment is running under a
    development type level of traceability.
    """

    return conf("LEVEL", "INFO") in ("DEBUG",)


def _is_secure():
    """
    Simple secure variable that should be overriden only under
    very specific/critical operation that control if the current
    environment should hide information that would otherwise
    compromise some of the system's secrecy (eg: version, name, etc.)

    :rtype: bool
    :return: If the current environment is running under a
    secured type level of traceability.
    """

    return conf("SECURE", True, cast=bool)


load()