mapres 2.4.dev6__tar.gz → 3a1__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.
mapres-3a1/PKG-INFO ADDED
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: mapres
3
+ Version: 3a1
4
+ Summary: A powerfull mapping utility
5
+ Author: iFamished
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/iFamished/mapres
8
+ Project-URL: Documentation, https://github.com/iFamished/mapres
9
+ Project-URL: Source, https://github.com/iFamished/mapres
10
+ Project-URL: Issues, https://github.com/iFamished/mapres/issues
11
+ Keywords: templating,automation,server-management
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Development Status :: 4 - Beta
19
+ Classifier: Intended Audience :: Developers
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: System :: Systems Administration
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # MapRes
28
+ A powerful string-mapping utility for many use cases.
29
+
30
+ ## Installation
31
+ ```bash
32
+ pip install mapres
33
+ ```
mapres-3a1/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # MapRes
2
+ A powerful string-mapping utility for many use cases.
3
+
4
+ ## Installation
5
+ ```bash
6
+ pip install mapres
7
+ ```
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "mapres"
10
- version = "2.4-dev.6"
10
+ version = "3-alpha.1"
11
11
  description = "A powerfull mapping utility"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.9"
@@ -18,13 +18,8 @@ keywords = [
18
18
  "templating",
19
19
  "automation",
20
20
  "server-management",
21
- "hungerlib",
22
- ]
23
- dependencies = [
24
- "requests",
25
- "mcrcon",
26
- "PyYaml",
27
21
  ]
22
+ dependencies = []
28
23
  classifiers = [
29
24
  "Programming Language :: Python :: 3",
30
25
  "Programming Language :: Python :: 3.10",
@@ -42,10 +37,10 @@ classifiers = [
42
37
  text = "MIT"
43
38
 
44
39
  [project.urls]
45
- Homepage = "https://github.com/iFamishedX/mapres"
46
- Documentation = "https://github.com/iFamishedX/mapres"
47
- Source = "https://github.com/iFamishedX/mapres"
48
- Issues = "https://github.com/iFamishedX/mapres/issues"
40
+ Homepage = "https://github.com/iFamished/mapres"
41
+ Documentation = "https://github.com/iFamished/mapres"
42
+ Source = "https://github.com/iFamished/mapres"
43
+ Issues = "https://github.com/iFamished/mapres/issues"
49
44
 
50
45
  [tool.setuptools.packages.find]
51
46
  where = [
@@ -7,7 +7,7 @@ try:
7
7
  except PackageNotFoundError:
8
8
  __version__ = '0.0.0'
9
9
 
10
- from .resolver import MapResolver, res, setGlobalMaps
10
+ from .resolver import MapResolver, res, rprint, setGlobalMaps
11
11
  from .datamap import DataMap, datamap, syntax
12
12
  from .layers import Layer, LayerStack
13
13
 
@@ -28,6 +28,10 @@ maps = SimpleNamespace(
28
28
  time = time,
29
29
  )
30
30
 
31
+ utils = SimpleNamespace(
32
+ rprint = rprint,
33
+ )
34
+
31
35
  __all__ = [
32
36
  # core modules
33
37
  'MapResolver',
@@ -39,6 +43,9 @@ __all__ = [
39
43
  'LayerStack',
40
44
  'setGlobalMaps',
41
45
 
46
+ # utils
47
+ 'rprint',
48
+
42
49
  # maps
43
50
  'ColorMap',
44
51
  'ascii_colors',
@@ -49,4 +56,5 @@ __all__ = [
49
56
 
50
57
  # namespaces
51
58
  'maps',
59
+ 'utils',
52
60
  ]
@@ -0,0 +1,29 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Node:
6
+ pass
7
+
8
+
9
+ @dataclass
10
+ class TemplateNode(Node):
11
+ children: list[Node]
12
+
13
+
14
+ @dataclass
15
+ class TextNode(Node):
16
+ text: str
17
+
18
+
19
+ @dataclass
20
+ class IdentNode(Node):
21
+ name: str
22
+ syntax: str
23
+
24
+
25
+ @dataclass
26
+ class CallNode(Node):
27
+ name: str # outer identifier
28
+ arg: Node # nested TemplateNode
29
+ syntax: str # which delimiter type produced it
@@ -0,0 +1,160 @@
1
+ from dataclasses import dataclass, fields, is_dataclass
2
+
3
+ # syntax identifiers
4
+ class syntax:
5
+ braces = 'braces' # {{value}}
6
+ dollars = 'dollars' # ${value}
7
+ angles = 'angles' # <value>
8
+ percents = 'percents' # %value%
9
+ pipes = 'pipes' # |value|
10
+ colons = 'colons' # :value:
11
+ at_tags = 'at_tags' # @value@
12
+ hash_tags = 'hash_tags' # #value#
13
+ paren_dollars = 'paren_dollars' # $(value)
14
+
15
+
16
+ # base datamap class
17
+ @dataclass
18
+ class DataMap:
19
+ '''
20
+ Core datamap class for mapres 3.
21
+
22
+ Responsibilities:
23
+ - hold fields
24
+ - optionally evaluate dynamic providers
25
+ - return a dict via as_map()
26
+
27
+ Syntax metadata (__syntax__) determines which delimiters
28
+ this map responds to (angle, brace, colon, percent, pipe, etc).
29
+
30
+ Mode metadata (__mode__) controls behavior:
31
+ - None: plain static map
32
+ - 'dynamic': values may be provided by callables/providers
33
+ - 'config': config-style maps (no dynamic evaluation)
34
+ - 'callable': map exposes callable methods for nested calls
35
+ '''
36
+ __syntax__: str = None
37
+ __mode__: str | None = None # 'dynamic', 'config', 'callable', or None
38
+ __missing_key__: str = 'error' # 'error', 'silent', 'placeholder'
39
+ __recursive__: bool = False
40
+ __max_depth__: int = 10
41
+
42
+ def as_map(self):
43
+ '''
44
+ Convert datamap fields into a dictionary.
45
+ If mode='dynamic', evaluate provider functions.
46
+
47
+ Callable mode does NOT change as_map() behavior; methods are
48
+ used by the evaluator, not exported as map entries.
49
+ '''
50
+ result = {}
51
+
52
+ # dataclass fields
53
+ for f in fields(self):
54
+ if not f.init or f.name.startswith('__'):
55
+ continue
56
+
57
+ val = getattr(self, f.name)
58
+
59
+ # dynamic provider support
60
+ if self.__mode__ == 'dynamic':
61
+ providers = getattr(self, 'providers', None)
62
+ if isinstance(providers, dict) and f.name in providers:
63
+ val = providers[f.name]
64
+ if callable(val):
65
+ val = val()
66
+
67
+ result[f.name] = val
68
+
69
+ # nested classes > nested dicts
70
+ for name, obj in self.__class__.__dict__.items():
71
+ if name.startswith('__'):
72
+ continue
73
+ if isinstance(obj, type):
74
+ result[name] = self._class_to_dict(obj)
75
+
76
+ return result
77
+
78
+ def _class_to_dict(self, cls):
79
+ out = {}
80
+
81
+ if is_dataclass(cls):
82
+ dummy = cls()
83
+ for f in fields(dummy):
84
+ if not f.init:
85
+ continue
86
+ out[f.name] = getattr(dummy, f.name)
87
+
88
+ for name, obj in cls.__dict__.items():
89
+ if name.startswith('__'):
90
+ continue
91
+ if isinstance(obj, type):
92
+ out[name] = self._class_to_dict(obj)
93
+
94
+ return out
95
+
96
+
97
+ def datamap(_cls=None, *, syntax=None, mode=None, missing_key=None, recursive=None, max_depth=None):
98
+ def wrap(cls):
99
+ cls.__syntax__ = syntax
100
+ cls.__mode__ = mode
101
+ cls.__missing_key__ = missing_key or 'error'
102
+ cls.__recursive__ = recursive or False
103
+ cls.__max_depth__ = max_depth or 10
104
+
105
+ namespace = dict(cls.__dict__)
106
+ namespace['__dict__'] = {}
107
+ rules_obj = namespace.get('rules', None)
108
+
109
+ cls = type(cls.__name__, (DataMap,), namespace)
110
+
111
+ if rules_obj is not None:
112
+ setattr(cls, 'rules', rules_obj)
113
+
114
+ return dataclass(frozen=False)(cls)
115
+
116
+ if _cls is not None and isinstance(_cls, type):
117
+ return wrap(_cls)
118
+
119
+ return wrap
120
+
121
+
122
+ # syntax-bound decorator factories
123
+ class _SyntaxFactory:
124
+ def __init__(self, syntax_name):
125
+ self._syntax = syntax_name
126
+
127
+ def __call__(self, _cls=None, **kwargs):
128
+ if _cls is not None and isinstance(_cls, type):
129
+ return datamap(_cls, syntax=self._syntax)
130
+ return datamap(syntax=self._syntax, **kwargs)
131
+
132
+ def config(self, _cls=None, **kwargs):
133
+ if _cls is not None and isinstance(_cls, type):
134
+ return datamap(_cls, syntax=self._syntax, mode='config')
135
+ return datamap(syntax=self._syntax, mode='config', **kwargs)
136
+
137
+ def dynamic(self, _cls=None, **kwargs):
138
+ if _cls is not None and isinstance(_cls, type):
139
+ return datamap(_cls, syntax=self._syntax, mode='dynamic')
140
+ return datamap(syntax=self._syntax, mode='dynamic', **kwargs)
141
+
142
+ def callable(self, _cls=None, **kwargs):
143
+ if _cls is not None and isinstance(_cls, type):
144
+ return datamap(_cls, syntax=self._syntax, mode='callable')
145
+ return datamap(syntax=self._syntax, mode='callable', **kwargs)
146
+
147
+
148
+ datamap.braces = _SyntaxFactory(syntax.braces)
149
+ datamap.dollars = _SyntaxFactory(syntax.dollars)
150
+ datamap.angles = _SyntaxFactory(syntax.angles)
151
+ datamap.percents = _SyntaxFactory(syntax.percents)
152
+ datamap.pipes = _SyntaxFactory(syntax.pipes)
153
+ datamap.colons = _SyntaxFactory(syntax.colons)
154
+ datamap.at_tags = _SyntaxFactory(syntax.at_tags)
155
+ datamap.hash_tags = _SyntaxFactory(syntax.hash_tags)
156
+ datamap.paren_dollars = _SyntaxFactory(syntax.paren_dollars)
157
+
158
+ datamap.config = datamap(mode='config')
159
+ datamap.dynamic = datamap(mode='dynamic')
160
+ datamap.callable = datamap(mode='callable')
@@ -0,0 +1,166 @@
1
+ from .ast import TemplateNode, TextNode, IdentNode, CallNode
2
+ from .tokenizer import Tokenizer
3
+ from .parser import Parser
4
+ from .exceptions import MapResError, MissingKeyError
5
+
6
+
7
+ class EvaluationError(MapResError):
8
+ pass
9
+
10
+
11
+ class Evaluator:
12
+ def __init__(self, layerstack, ctx=None):
13
+ self.layerstack = layerstack
14
+ self.ctx = ctx or {}
15
+ self._seen = set() # cycle detection
16
+
17
+ # public api
18
+ def evaluate(self, node):
19
+ return self._eval(node)
20
+
21
+ # internal dispatch
22
+ def _eval(self, node):
23
+ if isinstance(node, TemplateNode):
24
+ return self._eval_template(node)
25
+
26
+ if isinstance(node, TextNode):
27
+ return node.text
28
+
29
+ if isinstance(node, IdentNode):
30
+ return self._eval_ident(node)
31
+
32
+ if isinstance(node, CallNode):
33
+ return self._eval_call(node)
34
+
35
+ raise EvaluationError(f'Unknown AST node: {node}')
36
+
37
+ # re-resolve
38
+ def _re_resolve(self, text: str) -> str:
39
+ tokens = Tokenizer(text).tokenize()
40
+ ast = Parser(tokens).parse()
41
+ return self.evaluate(ast)
42
+
43
+
44
+ # template
45
+ def _eval_template(self, node: TemplateNode):
46
+ parts = []
47
+ for child in node.children:
48
+ parts.append(self._eval(child))
49
+ out = ''.join(parts)
50
+
51
+ # ---------------------------------------
52
+ # PER-MAP RECURSION (syntax-scoped)
53
+ # ---------------------------------------
54
+ # Find all maps in the layerstack that have recursive=True
55
+ recursive_maps = [
56
+ m for m in self.layerstack.all_maps()
57
+ if getattr(m, '__recursive__', False) is True
58
+ ]
59
+
60
+ # If no maps are recursive, return the first-pass output
61
+ if not recursive_maps:
62
+ return out
63
+
64
+ # Determine max recursion depth across all recursive maps
65
+ max_depth = max(getattr(m, '__max_depth__', 10) for m in recursive_maps)
66
+
67
+ seen = set()
68
+ depth = 0
69
+
70
+ while depth < max_depth:
71
+ # Re-tokenize and re-evaluate the ENTIRE output string
72
+ new_out = self._re_resolve(out)
73
+
74
+ # If stable, stop recursion
75
+ if new_out == out:
76
+ break
77
+
78
+ # Cycle detection
79
+ if new_out in seen:
80
+ raise EvaluationError('Cycle detected in recursive map resolution')
81
+
82
+ seen.add(new_out)
83
+ out = new_out
84
+ depth += 1
85
+
86
+ return out
87
+
88
+ # identifier
89
+ def _eval_ident(self, node: IdentNode):
90
+ name = node.name
91
+
92
+ # cycle detection
93
+ if name in self._seen:
94
+ raise EvaluationError(f"Cycle detected in identifier '{name}'")
95
+ self._seen.add(name)
96
+
97
+ parts = name.split('.')
98
+
99
+ # 1. ctx first
100
+ val = self._lookup_hierarchical(self.ctx, parts)
101
+ if val is not None:
102
+ self._seen.remove(name)
103
+ return str(val)
104
+
105
+ # 2. syntax-bound maps
106
+ for m in self.layerstack.all_maps():
107
+ if getattr(m, '__syntax__', None) not in (None, node.syntax):
108
+ continue
109
+
110
+ d = self._map_to_dict(m)
111
+ val = self._lookup_hierarchical(d, parts)
112
+ if val is not None:
113
+ self._seen.remove(name)
114
+ return str(val)
115
+
116
+ self._seen.remove(name)
117
+ raise MissingKeyError(name)
118
+
119
+ # call (nested token)
120
+ def _eval_call(self, node: CallNode):
121
+ outer = node.name
122
+
123
+ if outer in self._seen:
124
+ raise EvaluationError(f"Cycle detected in call '{outer}'")
125
+ self._seen.add(outer)
126
+
127
+ arg_value = self._eval(node.arg)
128
+ parts = [outer, arg_value]
129
+
130
+ # ctx first
131
+ val = self._lookup_hierarchical(self.ctx, parts)
132
+ if val is not None:
133
+ self._seen.remove(outer)
134
+ return str(val)
135
+
136
+ # syntax-bound maps
137
+ for m in self.layerstack.all_maps():
138
+ if getattr(m, '__syntax__', None) not in (None, node.syntax):
139
+ continue
140
+
141
+ d = self._map_to_dict(m)
142
+ val = self._lookup_hierarchical(d, parts)
143
+ if val is not None:
144
+ self._seen.remove(outer)
145
+ return str(val)
146
+
147
+ self._seen.remove(outer)
148
+ raise MissingKeyError(f'{outer}({arg_value})')
149
+
150
+ # helpers
151
+ def _map_to_dict(self, m):
152
+ if hasattr(m, 'as_map'):
153
+ return m.as_map()
154
+ if isinstance(m, dict):
155
+ return m
156
+ raise EvaluationError(f'Invalid map object: {m}')
157
+
158
+ def _lookup_hierarchical(self, root, parts):
159
+ cur = root
160
+ for p in parts:
161
+ if not isinstance(cur, dict):
162
+ return None
163
+ if p not in cur:
164
+ return None
165
+ cur = cur[p]
166
+ return cur
@@ -2,7 +2,9 @@ class MapResError(Exception):
2
2
  pass
3
3
 
4
4
  class MissingKeyError(MapResError):
5
- pass
5
+ def __init__(self, key):
6
+ super().__init__(f'Missing key {key!r}')
7
+ self.key = key
6
8
 
7
9
  class MapSyntaxError(MapResError):
8
10
  pass
@@ -0,0 +1,54 @@
1
+ class Layer:
2
+ """
3
+ A single layer containing one or more maps.
4
+ Priority determines resolution order.
5
+ Lower priority = earlier lookup.
6
+ """
7
+ def __init__(self, maps=None, priority=0):
8
+ self.maps = maps or []
9
+ self.priority = priority
10
+
11
+ def add(self, m):
12
+ self.maps.append(m)
13
+
14
+ def __iter__(self):
15
+ return iter(self.maps)
16
+
17
+
18
+ class LayerStack:
19
+ """
20
+ A simple ordered list of layers.
21
+ No names, no dicts, no complexity.
22
+ Just priority-sorted layers.
23
+ """
24
+ def __init__(self):
25
+ self.layers = [] # list of Layer objects
26
+
27
+ def add_layer(self, maps, priority=0):
28
+ """
29
+ Add a new layer containing one or more maps.
30
+ maps: datamap instance, datamap class, or dict
31
+ priority: lower = earlier lookup
32
+ """
33
+ layer = Layer(
34
+ maps=[maps] if not isinstance(maps, list) else maps,
35
+ priority=priority
36
+ )
37
+ self.layers.append(layer)
38
+ self.layers.sort(key=lambda l: l.priority)
39
+
40
+ def all_maps(self):
41
+ """
42
+ Yield all maps in priority order.
43
+ """
44
+ for layer in self.layers:
45
+ for m in layer:
46
+ yield m
47
+
48
+ def clone(self):
49
+ """
50
+ Return a shallow copy of the layerstack.
51
+ """
52
+ new = LayerStack()
53
+ new.layers = list(self.layers)
54
+ return new
@@ -1,6 +1,6 @@
1
- from mapres.datamap import datamap, syntax
1
+ from mapres.datamap import datamap
2
2
 
3
- @datamap(syntax=syntax.angles) # produces <>
3
+ @datamap.angles
4
4
  class ColorMap:
5
5
  black: str = '\033[30m'
6
6
  dark_blue: str = '\033[34m'
@@ -22,13 +22,8 @@ class ColorMap:
22
22
  bold: str = '\033[1m'
23
23
  italic: str = '\033[3m'
24
24
 
25
- def as_dict(self):
26
- return {f'<{k}>': getattr(self, k) for k in self.__dataclass_fields__}
27
-
28
- # ASCII color map
29
25
  ascii_colors = ColorMap()
30
26
 
31
- # Minecraft color map
32
27
  mc_colors = ColorMap(
33
28
  black='§0',
34
29
  dark_blue='§1',
@@ -51,7 +46,6 @@ mc_colors = ColorMap(
51
46
  italic='§o'
52
47
  )
53
48
 
54
- # Color strip map
55
49
  strip_colors = ColorMap(
56
50
  black='',
57
51
  dark_blue='',
@@ -72,4 +66,4 @@ strip_colors = ColorMap(
72
66
  reset='',
73
67
  bold='',
74
68
  italic=''
75
- )
69
+ )
@@ -1,6 +1,6 @@
1
1
  from datetime import datetime, timezone
2
2
  from zoneinfo import ZoneInfo
3
- from mapres.datamap import datamap, syntax
3
+ from mapres.datamap import datamap
4
4
 
5
5
 
6
6
  def safe_zoneinfo(tz: str):
@@ -10,7 +10,7 @@ def safe_zoneinfo(tz: str):
10
10
  return timezone.utc
11
11
 
12
12
 
13
- @datamap(syntax=syntax.percents, mode='dynamic')
13
+ @datamap.percents.dynamic
14
14
  class TimeMap:
15
15
  _default_tz = safe_zoneinfo("America/Chicago")
16
16