flyconf 0.1.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.
- flyconf-0.1.0/PKG-INFO +129 -0
- flyconf-0.1.0/README.md +115 -0
- flyconf-0.1.0/flyconf/__init__.py +13 -0
- flyconf-0.1.0/flyconf/__main__.py +8 -0
- flyconf-0.1.0/flyconf/lexer.py +245 -0
- flyconf-0.1.0/flyconf/model.py +93 -0
- flyconf-0.1.0/flyconf/parser.py +295 -0
- flyconf-0.1.0/flyconf/transformer.py +137 -0
- flyconf-0.1.0/flyconf.egg-info/PKG-INFO +129 -0
- flyconf-0.1.0/flyconf.egg-info/SOURCES.txt +18 -0
- flyconf-0.1.0/flyconf.egg-info/dependency_links.txt +1 -0
- flyconf-0.1.0/flyconf.egg-info/entry_points.txt +2 -0
- flyconf-0.1.0/flyconf.egg-info/requires.txt +4 -0
- flyconf-0.1.0/flyconf.egg-info/top_level.txt +1 -0
- flyconf-0.1.0/pyproject.toml +26 -0
- flyconf-0.1.0/setup.cfg +4 -0
- flyconf-0.1.0/tests/test_lexer.py +146 -0
- flyconf-0.1.0/tests/test_model.py +139 -0
- flyconf-0.1.0/tests/test_parser.py +169 -0
- flyconf-0.1.0/tests/test_transformer.py +141 -0
flyconf-0.1.0/PKG-INFO
ADDED
|
@@ -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` - 数据字典
|
flyconf-0.1.0/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# FlyConf Parser
|
|
2
|
+
|
|
3
|
+
FlyConf Parser 是一个用于解析fc配置文件格式的Python库。fc配置文件是一种具有特定语法的配置格式,支持复杂的数据结构、变量引用和环境感知配置。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 词法分析器,支持识别fc配置文件中的各种标记
|
|
8
|
+
- 语法分析器,构建配置块的抽象语法树
|
|
9
|
+
- 数据模型,用于表示解析后的配置结构
|
|
10
|
+
- 字符串处理(原生多行字符串和单行字符串)
|
|
11
|
+
- 变量引用系统(支持外部变量、环境变量和配置内引用)
|
|
12
|
+
- 列表解析(简单列表和嵌套列表)
|
|
13
|
+
- 导入导出功能(支持JSON格式)
|
|
14
|
+
- 环境感知配置合并
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install flyconf
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 使用方法
|
|
23
|
+
|
|
24
|
+
### 基本用法
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from flyconf.parser import FCConfigParser
|
|
28
|
+
|
|
29
|
+
# 解析fc配置文件
|
|
30
|
+
config = FCConfigParser.parse_file("config.fc")
|
|
31
|
+
|
|
32
|
+
# 访问配置块
|
|
33
|
+
block = config.get_block("server")
|
|
34
|
+
print(block.data)
|
|
35
|
+
|
|
36
|
+
# 导出为JSON
|
|
37
|
+
import json
|
|
38
|
+
json_data = config.to_json()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### fc配置文件语法
|
|
42
|
+
|
|
43
|
+
fc配置文件具有以下语法结构:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
@block_name(meta_key>meta_value) data_key>data_value
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
示例:
|
|
50
|
+
```fc
|
|
51
|
+
@mysql_default(type>conf.db)
|
|
52
|
+
dbtype>mysql
|
|
53
|
+
host>localhost
|
|
54
|
+
port>3306
|
|
55
|
+
database>oax
|
|
56
|
+
user>root
|
|
57
|
+
password>1234
|
|
58
|
+
|
|
59
|
+
@remember_me(type>conf.txt)
|
|
60
|
+
username>admin
|
|
61
|
+
password>1234
|
|
62
|
+
|
|
63
|
+
@test_list
|
|
64
|
+
users>[user1,user2,user3]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 字符串
|
|
68
|
+
|
|
69
|
+
使用 `^...^` 表示单行字符串,使用 `^^^...^^^` 表示多行字符串:
|
|
70
|
+
|
|
71
|
+
```fc
|
|
72
|
+
@get_users(type>conf.sql)
|
|
73
|
+
dbtype>mysql
|
|
74
|
+
sql>^
|
|
75
|
+
SELECT id, username, email FROM users
|
|
76
|
+
^
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 列表
|
|
80
|
+
|
|
81
|
+
使用 `[...]` 表示列表:
|
|
82
|
+
|
|
83
|
+
```fc
|
|
84
|
+
@test_list
|
|
85
|
+
users>[user1,user2,user3]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 变量引用
|
|
89
|
+
|
|
90
|
+
使用 `$(variable_name)` 表示变量引用:
|
|
91
|
+
|
|
92
|
+
```fc
|
|
93
|
+
@server
|
|
94
|
+
path>$(config.path)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API
|
|
98
|
+
|
|
99
|
+
### FCConfigParser
|
|
100
|
+
|
|
101
|
+
- `FCConfigParser.parse_text(text)` - 从文本解析配置
|
|
102
|
+
- `FCConfigParser.parse_file(file_path)` - 从文件解析配置
|
|
103
|
+
|
|
104
|
+
### FCConfig
|
|
105
|
+
|
|
106
|
+
- `config.get_block(name)` - 获取指定名称的块
|
|
107
|
+
- `config.add_block(block)` - 添加块
|
|
108
|
+
- `config.to_dict()` - 转换为字典
|
|
109
|
+
- `config.to_json()` - 转换为JSON字符串
|
|
110
|
+
|
|
111
|
+
### FCBlock
|
|
112
|
+
|
|
113
|
+
- `block.name` - 块名称
|
|
114
|
+
- `block.meta` - 元数据字典
|
|
115
|
+
- `block.data` - 数据字典
|
|
@@ -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"]
|
|
@@ -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
|
|
@@ -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)})"
|