flyconf 0.1.0__py3-none-any.whl

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.
flyconf/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ FlyConf Parser - A parser for fc configuration files.
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+ __author__ = "Your Name"
7
+ __email__ = "your.email@example.com"
8
+
9
+ from .parser import FCConfigParser
10
+ from .model import FCConfig, FCBlock
11
+ from .transformer import ConfigTransformer
12
+
13
+ __all__ = ["FCConfigParser", "FCConfig", "FCBlock", "ConfigTransformer"]
flyconf/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """
2
+ Main entry point for the flyconf module.
3
+ """
4
+
5
+ from . import main
6
+
7
+ if __name__ == "__main__":
8
+ main.main()
flyconf/lexer.py ADDED
@@ -0,0 +1,245 @@
1
+ """
2
+ Lexer for fc configuration files.
3
+ """
4
+
5
+ from enum import Enum
6
+ from typing import List, Iterator, NamedTuple
7
+ import re
8
+
9
+
10
+ class TokenType(Enum):
11
+ """Token types for fc configuration files."""
12
+ AT = "@" # @
13
+ BLOCK_NAME = "BLOCK_NAME" # Block name identifier
14
+ LPAREN = "(" # (
15
+ RPAREN = ")" # )
16
+ LBRACKET = "[" # [
17
+ RBRACKET = "]" # ]
18
+ GT = ">" # >
19
+ CARET = "^" # ^
20
+ DOUBLE_CARET = "^^" # ^^
21
+ HASH = "#" # #
22
+ DOLLAR_PAREN = "$(" # $(
23
+ IDENTIFIER = "IDENTIFIER" # General identifier
24
+ STRING = "STRING" # String content
25
+ COMMA = "," # ,
26
+ EQUALS = "=" # =
27
+ NEWLINE = "NEWLINE" # \n
28
+ WHITESPACE = "WHITESPACE" # Spaces, tabs
29
+ EOF = "EOF" # End of file
30
+
31
+
32
+ class Token(NamedTuple):
33
+ """Represents a token with type, value, and position."""
34
+ type: TokenType
35
+ value: str
36
+ line: int
37
+ column: int
38
+
39
+
40
+ class LexerError(Exception):
41
+ """Exception raised for lexer errors."""
42
+ pass
43
+
44
+
45
+ class Lexer:
46
+ """Lexer for fc configuration files."""
47
+
48
+ def __init__(self, text: str):
49
+ self.text: str = text
50
+ self.pos: int = 0
51
+ self.line: int = 1
52
+ self.column: int = 1
53
+ self.tokens: List[Token] = []
54
+
55
+ def _current_char(self) -> str:
56
+ """Get the current character."""
57
+ if self.pos >= len(self.text):
58
+ return '\0'
59
+ return self.text[self.pos]
60
+
61
+ def _peek_char(self, offset: int = 1) -> str:
62
+ """Peek at the next character."""
63
+ if self.pos + offset >= len(self.text):
64
+ return '\0'
65
+ return self.text[self.pos + offset]
66
+
67
+ def _advance(self) -> None:
68
+ """Advance the position."""
69
+ if self._current_char() == '\n':
70
+ self.line += 1
71
+ self.column = 1
72
+ else:
73
+ self.column += 1
74
+ self.pos += 1
75
+
76
+ def _skip_whitespace(self) -> None:
77
+ """Skip whitespace characters."""
78
+ while self._current_char().isspace() and self._current_char() != '\0':
79
+ if self._current_char() == '\n':
80
+ token = Token(TokenType.NEWLINE, '\n', self.line, self.column)
81
+ self.tokens.append(token)
82
+ self._advance()
83
+
84
+ def _read_identifier(self) -> str:
85
+ """Read an identifier."""
86
+ start = self.pos
87
+ while (self._current_char().isalnum() or
88
+ self._current_char() in ['_', '-', '.']) and \
89
+ self._current_char() != '\0':
90
+ self._advance()
91
+ return self.text[start:self.pos]
92
+
93
+ def _read_string(self, delimiter: str) -> str:
94
+ """Read a string until the delimiter."""
95
+ start = self.pos
96
+ while self._current_char() != delimiter and self._current_char() != '\0':
97
+ # Handle escaped characters
98
+ if self._current_char() == '\\' and self._peek_char() != '\0':
99
+ self._advance()
100
+ self._advance()
101
+
102
+ if self._current_char() == '\0':
103
+ raise LexerError(f"Unterminated string at line {self.line}, column {self.column}")
104
+
105
+ value = self.text[start:self.pos]
106
+ # Skip the closing delimiter
107
+ self._advance()
108
+ return value
109
+
110
+ def _read_multiline_string(self, delimiter: str) -> str:
111
+ """Read a multiline string until the delimiter."""
112
+ start = self.pos
113
+ # Look for the closing delimiter (two consecutive ^)
114
+ while not (self._current_char() == delimiter and
115
+ self._peek_char() == delimiter) and \
116
+ self._current_char() != '\0':
117
+ # Handle escaped characters
118
+ if self._current_char() == '\\' and self._peek_char() != '\0':
119
+ self._advance()
120
+ self._advance()
121
+
122
+ if self._current_char() == '\0':
123
+ raise LexerError(f"Unterminated multiline string at line {self.line}, column {self.column}")
124
+
125
+ value = self.text[start:self.pos]
126
+ # Skip the closing delimiters
127
+ self._advance() # Skip first ^
128
+ self._advance() # Skip second ^
129
+ return value
130
+
131
+ def _read_raw_string(self, delimiter: str) -> str:
132
+ """Read a raw string until the delimiter."""
133
+ start = self.pos
134
+ # Look for the closing delimiter (three consecutive ^)
135
+ while not (self._current_char() == delimiter and
136
+ self._peek_char() == delimiter and
137
+ self._peek_char(2) == delimiter) and \
138
+ self._current_char() != '\0':
139
+ self._advance()
140
+
141
+ if self._current_char() == '\0':
142
+ raise LexerError(f"Unterminated raw string at line {self.line}, column {self.column}")
143
+
144
+ value = self.text[start:self.pos]
145
+ # Skip the closing delimiters
146
+ self._advance() # Skip first ^
147
+ self._advance() # Skip second ^
148
+ self._advance() # Skip third ^
149
+ return value
150
+
151
+ def _skip_comment(self) -> None:
152
+ """Skip a comment until end of line."""
153
+ while self._current_char() != '\n' and self._current_char() != '\0':
154
+ self._advance()
155
+
156
+ def tokenize(self) -> List[Token]:
157
+ """Tokenize the input text."""
158
+ while self._current_char() != '\0':
159
+ # Skip whitespace but capture newlines
160
+ self._skip_whitespace()
161
+
162
+ if self._current_char() == '\0':
163
+ break
164
+
165
+ # Handle comments
166
+ if self._current_char() == '#':
167
+ self._skip_comment()
168
+ continue
169
+
170
+ # Handle special tokens
171
+ char = self._current_char()
172
+ next_char = self._peek_char()
173
+ next_next_char = self._peek_char(2)
174
+
175
+ # Handle triple caret (raw string) (^^^)
176
+ if char == '^' and next_char == '^' and next_next_char == '^':
177
+ self._advance()
178
+ self._advance()
179
+ self._advance()
180
+ # Read raw string
181
+ string_value = self._read_raw_string('^')
182
+ string_token = Token(TokenType.STRING, string_value, self.line, self.column)
183
+ self.tokens.append(string_token)
184
+ continue
185
+
186
+ # Handle double caret (multiline string) (^^)
187
+ if char == '^' and next_char == '^' and next_next_char != '^':
188
+ self._advance()
189
+ self._advance()
190
+ # Read multiline string
191
+ string_value = self._read_multiline_string('^')
192
+ string_token = Token(TokenType.STRING, string_value, self.line, self.column)
193
+ self.tokens.append(string_token)
194
+ continue
195
+
196
+ # Handle single caret (^)
197
+ if char == '^':
198
+ self._advance()
199
+ # Read string
200
+ string_value = self._read_string('^')
201
+ string_token = Token(TokenType.STRING, string_value, self.line, self.column)
202
+ self.tokens.append(string_token)
203
+ continue
204
+
205
+ # Handle other special tokens
206
+ special_tokens = {
207
+ '@': TokenType.AT,
208
+ '(': TokenType.LPAREN,
209
+ ')': TokenType.RPAREN,
210
+ '[': TokenType.LBRACKET,
211
+ ']': TokenType.RBRACKET,
212
+ '>': TokenType.GT,
213
+ ',': TokenType.COMMA,
214
+ '=': TokenType.EQUALS,
215
+ '#': TokenType.HASH,
216
+ }
217
+
218
+ # Handle $(
219
+ if char == '$' and next_char == '(':
220
+ token = Token(TokenType.DOLLAR_PAREN, '$(', self.line, self.column)
221
+ self.tokens.append(token)
222
+ self._advance()
223
+ self._advance()
224
+ continue
225
+
226
+ # Handle other special tokens
227
+ if char in special_tokens:
228
+ token = Token(special_tokens[char], char, self.line, self.column)
229
+ self.tokens.append(token)
230
+ self._advance()
231
+ continue
232
+
233
+ # Handle identifiers
234
+ if char.isalnum() or char == '_':
235
+ value = self._read_identifier()
236
+ token = Token(TokenType.IDENTIFIER, value, self.line, self.column)
237
+ self.tokens.append(token)
238
+ continue
239
+
240
+ # Skip unknown characters
241
+ self._advance()
242
+
243
+ # Add EOF token
244
+ self.tokens.append(Token(TokenType.EOF, '', self.line, self.column))
245
+ return self.tokens
flyconf/model.py ADDED
@@ -0,0 +1,93 @@
1
+ """
2
+ Data models for fc configuration files.
3
+ """
4
+
5
+ from typing import List, Dict, Any, Optional
6
+
7
+
8
+ class FCBlock:
9
+ """
10
+ Represents a block in fc configuration file.
11
+
12
+ A block consists of:
13
+ - name: Block identifier
14
+ - meta: Metadata dictionary
15
+ - data: Data dictionary containing key-value pairs
16
+ """
17
+
18
+ def __init__(self, name: str):
19
+ self.name: str = name
20
+ self.meta: Dict[str, Any] = {}
21
+ self.data: Dict[str, Any] = {}
22
+
23
+ def __repr__(self) -> str:
24
+ return f"FCBlock(name='{self.name}', meta={self.meta}, data={self.data})"
25
+
26
+
27
+ class FCConfig:
28
+ """
29
+ Represents a complete fc configuration.
30
+
31
+ Contains:
32
+ - blocks: List of FCBlock objects
33
+ - variables: Global variables dictionary
34
+ """
35
+
36
+ def __init__(self):
37
+ self.blocks: List[FCBlock] = []
38
+ self.variables: Dict[str, Any] = {}
39
+
40
+ def get_block(self, name: str) -> Optional[FCBlock]:
41
+ """Get a block by name."""
42
+ for block in self.blocks:
43
+ if block.name == name:
44
+ return block
45
+ return None
46
+
47
+ def add_block(self, block: FCBlock) -> None:
48
+ """Add a block to the configuration."""
49
+ self.blocks.append(block)
50
+
51
+ def to_dict(self) -> Dict[str, Any]:
52
+ """Convert the configuration to a dictionary."""
53
+ result = {
54
+ "variables": self.variables,
55
+ "blocks": {}
56
+ }
57
+
58
+ for block in self.blocks:
59
+ result["blocks"][block.name] = {
60
+ "meta": block.meta,
61
+ "data": block.data
62
+ }
63
+
64
+ return result
65
+
66
+ def to_json(self) -> str:
67
+ """Convert the configuration to JSON string."""
68
+ import json
69
+ return json.dumps(self.to_dict(), indent=2, ensure_ascii=False)
70
+
71
+ @classmethod
72
+ def from_dict(cls, data: Dict[str, Any]) -> "FCConfig":
73
+ """Create a configuration from a dictionary."""
74
+ config = cls()
75
+ config.variables = data.get("variables", {})
76
+
77
+ for block_name, block_data in data.get("blocks", {}).items():
78
+ block = FCBlock(block_name)
79
+ block.meta = block_data.get("meta", {})
80
+ block.data = block_data.get("data", {})
81
+ config.add_block(block)
82
+
83
+ return config
84
+
85
+ @classmethod
86
+ def from_json(cls, json_str: str) -> "FCConfig":
87
+ """Create a configuration from a JSON string."""
88
+ import json
89
+ data = json.loads(json_str)
90
+ return cls.from_dict(data)
91
+
92
+ def __repr__(self) -> str:
93
+ return f"FCConfig(blocks={len(self.blocks)}, variables={len(self.variables)})"
flyconf/parser.py ADDED
@@ -0,0 +1,295 @@
1
+ from typing import List, Dict, Any, Optional
2
+ from .lexer import Lexer, Token, TokenType
3
+ from .model import FCConfig, FCBlock
4
+
5
+
6
+ class ParserError(Exception):
7
+ """Exception raised for parser errors."""
8
+ pass
9
+
10
+
11
+ class FCConfigParser:
12
+ """Parser for fc configuration files."""
13
+
14
+ def __init__(self, text: str):
15
+ self.lexer = Lexer(text)
16
+ self.tokens: List[Token] = []
17
+ self.pos: int = 0
18
+ self.config: FCConfig = FCConfig()
19
+
20
+ def _current_token(self) -> Token:
21
+ """Get the current token."""
22
+ if self.pos >= len(self.tokens):
23
+ return Token(TokenType.EOF, '', 0, 0)
24
+ return self.tokens[self.pos]
25
+
26
+ def _peek_token(self, offset: int = 1) -> Token:
27
+ """Peek at the next token."""
28
+ if self.pos + offset >= len(self.tokens):
29
+ return Token(TokenType.EOF, '', 0, 0)
30
+ return self.tokens[self.pos + offset]
31
+
32
+ def _consume_token(self) -> Token:
33
+ """Consume the current token and advance."""
34
+ token = self._current_token()
35
+ self.pos += 1
36
+ return token
37
+
38
+ def _match_token(self, token_type: TokenType) -> bool:
39
+ """Check if the current token matches the expected type."""
40
+ if self._current_token().type == token_type:
41
+ self._consume_token()
42
+ return True
43
+ return False
44
+
45
+ def _expect_token(self, token_type: TokenType) -> Token:
46
+ """Expect a specific token type or raise an error."""
47
+ if self._current_token().type == token_type:
48
+ return self._consume_token()
49
+ raise ParserError(
50
+ f"Expected token {token_type}, got {self._current_token().type} "
51
+ f"at line {self._current_token().line}, column {self._current_token().column}"
52
+ )
53
+
54
+ def _parse_block_name(self) -> str:
55
+ """Parse a block name."""
56
+ token = self._expect_token(TokenType.IDENTIFIER)
57
+ return token.value
58
+
59
+ def _parse_key_value_list(self) -> Dict[str, Any]:
60
+ """Parse a key-value list."""
61
+ result = {}
62
+
63
+ while self._current_token().type not in [TokenType.RPAREN, TokenType.RBRACKET, TokenType.EOF]:
64
+ # Skip newlines
65
+ if self._current_token().type == TokenType.NEWLINE:
66
+ self._consume_token()
67
+ continue
68
+
69
+ # Check if we've reached the start of a new block
70
+ if self._current_token().type == TokenType.AT:
71
+ break
72
+
73
+ # Check if the first token is a list (special case for lists without keys)
74
+ if self._current_token().type == TokenType.LBRACKET:
75
+ # This is a list without a key, which is not valid in our format
76
+ raise ParserError(
77
+ f"Unexpected list at line {self._current_token().line}, column {self._current_token().column}. "
78
+ f"Lists must be associated with a key."
79
+ )
80
+
81
+ # Parse key
82
+ key_token = self._expect_token(TokenType.IDENTIFIER)
83
+ key = key_token.value
84
+
85
+ # Check if there's a value (indicated by > or directly a list)
86
+ if self._current_token().type == TokenType.GT:
87
+ self._consume_token() # Consume >
88
+ # Parse value
89
+ value = self._parse_value()
90
+ result[key] = value
91
+ elif self._current_token().type == TokenType.LBRACKET:
92
+ # Direct list value without >
93
+ value = self._parse_value()
94
+ result[key] = value
95
+ else:
96
+ # No value, treat as a flag
97
+ result[key] = True
98
+
99
+ # Optional comma
100
+ if self._current_token().type == TokenType.COMMA:
101
+ self._consume_token()
102
+ # Handle trailing comma
103
+ if self._current_token().type in [TokenType.RPAREN, TokenType.RBRACKET, TokenType.EOF]:
104
+ break
105
+
106
+ return result
107
+
108
+ def _parse_value(self) -> Any:
109
+ """Parse a value."""
110
+ token = self._current_token()
111
+
112
+ # Handle string
113
+ if token.type == TokenType.STRING:
114
+ return self._consume_token().value
115
+
116
+ # Handle list
117
+ if token.type == TokenType.LBRACKET:
118
+ return self._parse_list()
119
+
120
+ # Handle variable reference
121
+ if token.type == TokenType.DOLLAR_PAREN:
122
+ return self._parse_variable_reference()
123
+
124
+ # Handle identifier (can be a simple value or part of a list)
125
+ if token.type == TokenType.IDENTIFIER:
126
+ return self._consume_token().value
127
+
128
+ # Default - consume and return as string
129
+ return self._consume_token().value
130
+
131
+ def _parse_list_item(self) -> Dict[str, Any]:
132
+ """Parse a single list item (object with key-value pairs)."""
133
+ result = {}
134
+
135
+ # Parse key-value pairs until we hit a comma, bracket, or EOF
136
+ while self._current_token().type not in [TokenType.COMMA, TokenType.RBRACKET, TokenType.EOF]:
137
+ # Skip newlines
138
+ if self._current_token().type == TokenType.NEWLINE:
139
+ self._consume_token()
140
+ continue
141
+
142
+ # Check if we've reached the start of a new block
143
+ if self._current_token().type == TokenType.AT:
144
+ break
145
+
146
+ # Parse key
147
+ key_token = self._expect_token(TokenType.IDENTIFIER)
148
+ key = key_token.value
149
+
150
+ # Check if there's a value (indicated by >)
151
+ if self._current_token().type == TokenType.GT:
152
+ self._consume_token() # Consume >
153
+ # Parse value
154
+ value = self._parse_value()
155
+ result[key] = value
156
+ else:
157
+ # No value, treat as a flag
158
+ result[key] = True
159
+
160
+ return result
161
+
162
+ def _parse_list(self) -> List[Any]:
163
+ """Parse a list."""
164
+ # Expect [
165
+ self._expect_token(TokenType.LBRACKET)
166
+
167
+ items = []
168
+ while self._current_token().type != TokenType.RBRACKET and \
169
+ self._current_token().type != TokenType.EOF:
170
+
171
+ # Skip newlines
172
+ if self._current_token().type == TokenType.NEWLINE:
173
+ self._consume_token()
174
+ continue
175
+
176
+ # Check if we've reached the start of a new block
177
+ if self._current_token().type == TokenType.AT:
178
+ break
179
+
180
+ # Handle comma separator between list items
181
+ if self._current_token().type == TokenType.COMMA:
182
+ self._consume_token()
183
+ continue
184
+
185
+ # Check if this is the start of a list item (key-value pairs)
186
+ if self._current_token().type == TokenType.IDENTIFIER:
187
+ # Parse list item as an object
188
+ item = self._parse_list_item()
189
+ items.append(item)
190
+ elif self._current_token().type == TokenType.RBRACKET:
191
+ # End of list
192
+ break
193
+ else:
194
+ # Unexpected token in list
195
+ raise ParserError(
196
+ f"Unexpected token {self._current_token().type} in list "
197
+ f"at line {self._current_token().line}, column {self._current_token().column}"
198
+ )
199
+
200
+ # Expect ]
201
+ self._expect_token(TokenType.RBRACKET)
202
+ return items
203
+
204
+ def _parse_variable_reference(self) -> str:
205
+ """Parse a variable reference."""
206
+ # Expect $(
207
+ self._expect_token(TokenType.DOLLAR_PAREN)
208
+
209
+ # Parse variable name
210
+ var_name = ""
211
+ while self._current_token().type != TokenType.EOF and \
212
+ self._current_token().type != TokenType.RPAREN:
213
+ var_name += self._consume_token().value
214
+
215
+ # Expect )
216
+ self._expect_token(TokenType.RPAREN)
217
+
218
+ return f"$({var_name})"
219
+
220
+ def _parse_meta_part(self) -> Dict[str, Any]:
221
+ """Parse the meta part of a block."""
222
+ # Expect (
223
+ self._expect_token(TokenType.LPAREN)
224
+
225
+ # Parse key-value list
226
+ meta = self._parse_key_value_list()
227
+
228
+ # Expect )
229
+ self._expect_token(TokenType.RPAREN)
230
+
231
+ return meta
232
+
233
+ def _parse_data_part(self) -> Dict[str, Any]:
234
+ """Parse the data part of a block."""
235
+ # Parse key-value list directly (no braces)
236
+ return self._parse_key_value_list()
237
+
238
+ def _parse_block(self) -> FCBlock:
239
+ """Parse a block."""
240
+ # Expect @
241
+ self._expect_token(TokenType.AT)
242
+
243
+ # Parse block name
244
+ block_name = self._parse_block_name()
245
+ block = FCBlock(block_name)
246
+
247
+ # Optional meta part
248
+ if self._current_token().type == TokenType.LPAREN:
249
+ block.meta = self._parse_meta_part()
250
+
251
+ # Skip newlines before data part
252
+ while self._current_token().type == TokenType.NEWLINE:
253
+ self._consume_token()
254
+
255
+ # Optional data part - check if the next token is an identifier (start of key-value pair)
256
+ # Lists are not allowed directly in the data part without a key
257
+ if self._current_token().type == TokenType.IDENTIFIER:
258
+ block.data = self._parse_data_part()
259
+
260
+ return block
261
+
262
+ def parse(self) -> FCConfig:
263
+ """Parse the entire configuration."""
264
+ # Tokenize the input
265
+ self.tokens = self.lexer.tokenize()
266
+
267
+ # Parse blocks
268
+ while self._current_token().type != TokenType.EOF:
269
+ # Skip newlines
270
+ if self._current_token().type == TokenType.NEWLINE:
271
+ self._consume_token()
272
+ continue
273
+
274
+ # Parse block
275
+ if self._current_token().type == TokenType.AT:
276
+ block = self._parse_block()
277
+ self.config.add_block(block)
278
+ else:
279
+ # Skip unknown tokens
280
+ self._consume_token()
281
+
282
+ return self.config
283
+
284
+ @classmethod
285
+ def parse_text(cls, text: str) -> FCConfig:
286
+ """Parse configuration from text."""
287
+ parser = cls(text)
288
+ return parser.parse()
289
+
290
+ @classmethod
291
+ def parse_file(cls, file_path: str) -> FCConfig:
292
+ """Parse configuration from a file."""
293
+ with open(file_path, 'r', encoding='utf-8') as f:
294
+ text = f.read()
295
+ return cls.parse_text(text)
flyconf/transformer.py ADDED
@@ -0,0 +1,137 @@
1
+ """
2
+ Transformer for fc configuration files.
3
+ Handles variable resolution and advanced transformations.
4
+ """
5
+
6
+ import os
7
+ from typing import Any, Dict
8
+ from .model import FCConfig, FCBlock
9
+
10
+
11
+ class VariableResolver:
12
+ """Resolves variable references in the configuration."""
13
+
14
+ def __init__(self, config: FCConfig):
15
+ self.config = config
16
+
17
+ def resolve_variable(self, var_ref: str) -> str:
18
+ """Resolve a variable reference."""
19
+ # Remove the $() wrapper
20
+ if var_ref.startswith("$(") and var_ref.endswith(")"):
21
+ var_name = var_ref[2:-1]
22
+ else:
23
+ var_name = var_ref
24
+
25
+ # Handle environment variables
26
+ if var_name.startswith("ENV:"):
27
+ env_var_name = var_name[4:] # Remove "ENV:" prefix
28
+ return os.getenv(env_var_name, "")
29
+
30
+ # Handle internal references (this.block_name.key)
31
+ if var_name.startswith("this."):
32
+ parts = var_name[5:].split(".") # Remove "this." prefix
33
+ if len(parts) >= 2:
34
+ block_name = parts[0]
35
+ key = ".".join(parts[1:]) # Join remaining parts with dots
36
+
37
+ block = self.config.get_block(block_name)
38
+ if block:
39
+ # Navigate through nested dictionaries if needed
40
+ value = block.data
41
+ key_parts = key.split(".")
42
+ try:
43
+ for part in key_parts:
44
+ if isinstance(value, dict) and part in value:
45
+ value = value[part]
46
+ else:
47
+ return var_ref # Return original if not found
48
+ return str(value)
49
+ except (KeyError, TypeError):
50
+ return var_ref # Return original if error occurs
51
+
52
+ # Handle global variables
53
+ if var_name in self.config.variables:
54
+ return str(self.config.variables[var_name])
55
+
56
+ # Variable not found
57
+ return var_ref
58
+
59
+ def resolve_value(self, value: Any) -> Any:
60
+ """Recursively resolve variables in a value."""
61
+ if isinstance(value, str):
62
+ if value.startswith("$(") and value.endswith(")"):
63
+ return self.resolve_variable(value)
64
+ return value
65
+ elif isinstance(value, dict):
66
+ resolved = {}
67
+ for k, v in value.items():
68
+ resolved[k] = self.resolve_value(v)
69
+ return resolved
70
+ elif isinstance(value, list):
71
+ return [self.resolve_value(item) for item in value]
72
+ else:
73
+ return value
74
+
75
+ def resolve_config(self) -> FCConfig:
76
+ """Resolve all variables in the configuration."""
77
+ resolved_config = FCConfig()
78
+ resolved_config.variables = self.config.variables.copy()
79
+
80
+ # Copy and resolve each block
81
+ for block in self.config.blocks:
82
+ resolved_block = FCBlock(block.name)
83
+ resolved_block.meta = block.meta.copy()
84
+
85
+ # Resolve data values
86
+ for key, value in block.data.items():
87
+ resolved_block.data[key] = self.resolve_value(value)
88
+
89
+ resolved_config.add_block(resolved_block)
90
+
91
+ return resolved_config
92
+
93
+
94
+ class ConfigTransformer:
95
+ """Transforms and processes FCConfig objects."""
96
+
97
+ @staticmethod
98
+ def resolve_variables(config: FCConfig) -> FCConfig:
99
+ """Resolve all variable references in the configuration."""
100
+ resolver = VariableResolver(config)
101
+ return resolver.resolve_config()
102
+
103
+ @staticmethod
104
+ def apply_environment(config: FCConfig, env_name: str = None) -> FCConfig:
105
+ """Apply environment-specific configurations."""
106
+ # For now, we'll just return the config as-is
107
+ # In a full implementation, this would merge environment-specific blocks
108
+ return config
109
+
110
+ @staticmethod
111
+ def merge_configs(base_config: FCConfig, override_config: FCConfig) -> FCConfig:
112
+ """Merge two configurations, with override_config taking precedence."""
113
+ merged_config = FCConfig()
114
+
115
+ # Copy base variables and update with override variables
116
+ merged_config.variables = base_config.variables.copy()
117
+ merged_config.variables.update(override_config.variables)
118
+
119
+ # Create a dict of base blocks for easy lookup
120
+ base_blocks = {block.name: block for block in base_config.blocks}
121
+
122
+ # Add all base blocks
123
+ for block in base_config.blocks:
124
+ merged_config.add_block(block)
125
+
126
+ # Add or replace with override blocks
127
+ for override_block in override_config.blocks:
128
+ existing_block = merged_config.get_block(override_block.name)
129
+ if existing_block:
130
+ # Replace existing block
131
+ existing_block.meta.update(override_block.meta)
132
+ existing_block.data.update(override_block.data)
133
+ else:
134
+ # Add new block
135
+ merged_config.add_block(override_block)
136
+
137
+ return merged_config
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: flyconf
3
+ Version: 0.1.0
4
+ Summary: A parser for fc configuration files with support for complex structures, variables, and environment-aware configurations
5
+ Author-email: Your Name <your.email@example.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourusername/flyconf
8
+ Project-URL: Repository, https://github.com/yourusername/flyconf
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=7.0; extra == "dev"
13
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
14
+
15
+ # FlyConf Parser
16
+
17
+ FlyConf Parser 是一个用于解析fc配置文件格式的Python库。fc配置文件是一种具有特定语法的配置格式,支持复杂的数据结构、变量引用和环境感知配置。
18
+
19
+ ## 特性
20
+
21
+ - 词法分析器,支持识别fc配置文件中的各种标记
22
+ - 语法分析器,构建配置块的抽象语法树
23
+ - 数据模型,用于表示解析后的配置结构
24
+ - 字符串处理(原生多行字符串和单行字符串)
25
+ - 变量引用系统(支持外部变量、环境变量和配置内引用)
26
+ - 列表解析(简单列表和嵌套列表)
27
+ - 导入导出功能(支持JSON格式)
28
+ - 环境感知配置合并
29
+
30
+ ## 安装
31
+
32
+ ```bash
33
+ pip install flyconf
34
+ ```
35
+
36
+ ## 使用方法
37
+
38
+ ### 基本用法
39
+
40
+ ```python
41
+ from flyconf.parser import FCConfigParser
42
+
43
+ # 解析fc配置文件
44
+ config = FCConfigParser.parse_file("config.fc")
45
+
46
+ # 访问配置块
47
+ block = config.get_block("server")
48
+ print(block.data)
49
+
50
+ # 导出为JSON
51
+ import json
52
+ json_data = config.to_json()
53
+ ```
54
+
55
+ ### fc配置文件语法
56
+
57
+ fc配置文件具有以下语法结构:
58
+
59
+ ```
60
+ @block_name(meta_key>meta_value) data_key>data_value
61
+ ```
62
+
63
+ 示例:
64
+ ```fc
65
+ @mysql_default(type>conf.db)
66
+ dbtype>mysql
67
+ host>localhost
68
+ port>3306
69
+ database>oax
70
+ user>root
71
+ password>1234
72
+
73
+ @remember_me(type>conf.txt)
74
+ username>admin
75
+ password>1234
76
+
77
+ @test_list
78
+ users>[user1,user2,user3]
79
+ ```
80
+
81
+ ### 字符串
82
+
83
+ 使用 `^...^` 表示单行字符串,使用 `^^^...^^^` 表示多行字符串:
84
+
85
+ ```fc
86
+ @get_users(type>conf.sql)
87
+ dbtype>mysql
88
+ sql>^
89
+ SELECT id, username, email FROM users
90
+ ^
91
+ ```
92
+
93
+ ### 列表
94
+
95
+ 使用 `[...]` 表示列表:
96
+
97
+ ```fc
98
+ @test_list
99
+ users>[user1,user2,user3]
100
+ ```
101
+
102
+ ### 变量引用
103
+
104
+ 使用 `$(variable_name)` 表示变量引用:
105
+
106
+ ```fc
107
+ @server
108
+ path>$(config.path)
109
+ ```
110
+
111
+ ## API
112
+
113
+ ### FCConfigParser
114
+
115
+ - `FCConfigParser.parse_text(text)` - 从文本解析配置
116
+ - `FCConfigParser.parse_file(file_path)` - 从文件解析配置
117
+
118
+ ### FCConfig
119
+
120
+ - `config.get_block(name)` - 获取指定名称的块
121
+ - `config.add_block(block)` - 添加块
122
+ - `config.to_dict()` - 转换为字典
123
+ - `config.to_json()` - 转换为JSON字符串
124
+
125
+ ### FCBlock
126
+
127
+ - `block.name` - 块名称
128
+ - `block.meta` - 元数据字典
129
+ - `block.data` - 数据字典
@@ -0,0 +1,11 @@
1
+ flyconf/__init__.py,sha256=_PT5-vFjh_YVnoYsLA1y1sVQDx3R-OlzzuGuxbXLd9I,336
2
+ flyconf/__main__.py,sha256=gQnC64sonT2uTaPysH9xAoDWmBtkJuuXryCku2JJCtQ,112
3
+ flyconf/lexer.py,sha256=UAqB0-ZcJokmGDJoy8Pr5B5q-EIxjhxrEBdyfa30jdI,8585
4
+ flyconf/model.py,sha256=6OQjTJDXQDemWpUIeBDt9-iElroFn0bvkn-VRnoi2kY,2666
5
+ flyconf/parser.py,sha256=RYEApDRcVHHmSRwRg56v25h4EBdBMnY5v64FJl1RBBM,10674
6
+ flyconf/transformer.py,sha256=qvnIh96DCVTJ2xaxCFskEzrDsuNMUEpmsHUG-0zq-2Y,5207
7
+ flyconf-0.1.0.dist-info/METADATA,sha256=1titiEHcU7oCOhSz3LjNvJ2ZbWaP8IUUOqgdpoltEUg,2868
8
+ flyconf-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ flyconf-0.1.0.dist-info/entry_points.txt,sha256=_Li-kCD7D12azPA5hhPYlZ9rZX0LcfkHO3jjXMkc5TE,50
10
+ flyconf-0.1.0.dist-info/top_level.txt,sha256=kYvV-mkBd_1kvtge2WbuH6VKn5Uodp72rZM4g2W6oZE,8
11
+ flyconf-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ flyconf = flyconf.__main__:main
@@ -0,0 +1 @@
1
+ flyconf