docdsl 0.0.1__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.
docdsl-0.0.1/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2026 Shine Jayakumar
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
docdsl-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: docdsl
3
+ Version: 0.0.1
4
+ Summary: A domain-specific language (DSL) for building readable text extraction rules.
5
+ Author-email: Shine Jayakumar <shinejayakumar@yahoo.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/shine-jayakumar/docdsl
8
+ Project-URL: Repository, https://github.com/shine-jayakumar/docdsl
9
+ Project-URL: Documentation, https://github.com/shine-jayakumar/docdsl/blob/master/Documentation.md
10
+ Project-URL: Issues, https://github.com/shine-jayakumar/docdsl/issues
11
+ Keywords: dsl,regex,parser,text-extraction,pattern-matching,textx
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Classifier: Topic :: Text Processing
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: textX<5,>=4.4
23
+ Dynamic: license-file
24
+
25
+ # DocDSL
26
+
27
+ ![License](https://img.shields.io/static/v1?label=license&message=MIT&color=green)
28
+ ![Open Source](https://img.shields.io/static/v1?label=OpenSource&message=Yes&color=brightgreen)
29
+ ![Version](https://img.shields.io/static/v1?label=version&message=v.0.0.1&color=blue)
30
+ ![Status](https://img.shields.io/badge/status-alpha-yellow.svg)
31
+
32
+
33
+ A declarative DSL for extracting structured information from OCR output, PDFs, reports, and other semi-structured documents.
34
+
35
+ Instead of writing large, difficult-to-maintain regular expressions, **DocDSL** lets you describe **what** to extract using a simple, readable language while keeping extraction rules separate from your application code.
36
+
37
+ ---
38
+
39
+ # Installation
40
+
41
+ ```bash
42
+ pip install docdsl
43
+ ```
44
+
45
+ ---
46
+
47
+ # Quick Start
48
+
49
+ ## 1. Define your entities
50
+
51
+ Entities are reusable regular expressions that can be referenced throughout your DSL.
52
+
53
+ ```python
54
+ from docdsl import DSLTranslator, Entity
55
+
56
+ NAME = Entity(
57
+ name="NAME",
58
+ pattern=r"[A-Za-z ,.'-]+"
59
+ )
60
+
61
+ POSTCODE = Entity(
62
+ name="POSTCODE",
63
+ pattern=r"[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}"
64
+ )
65
+ ```
66
+
67
+ ## 2. Create a translator
68
+
69
+ ```python
70
+ translator = DSLTranslator(
71
+ entities=[
72
+ NAME,
73
+ POSTCODE,
74
+ ]
75
+ )
76
+ ```
77
+
78
+ ## 3. Write your extraction rules
79
+
80
+ ```python
81
+ dsl = """
82
+ FIND "Name:";
83
+ SKIP UNTIL NEWLINE;
84
+ CAPTURE TARGET [@NAME];
85
+ """
86
+ ```
87
+
88
+ ## 4. Generate the regular expression
89
+
90
+ ```python
91
+ pattern = translator.translate(dsl)
92
+ ```
93
+
94
+ Your DSL is translated into a regular expression that can be used as part of your document extraction pipeline.
95
+
96
+ ---
97
+
98
+ # Features
99
+
100
+ - Declarative, English-like extraction language
101
+ - Reusable named entities
102
+ - Readable extraction rules
103
+ - Conditional extraction using `IF`
104
+ - Multi-line capture
105
+ - Capture between delimiters
106
+ - Built-in helper tokens
107
+ - Friendly syntax and validation errors
108
+ - Exact error locations with line and column information
109
+
110
+ ---
111
+
112
+ # Entities
113
+
114
+ Entities represent reusable regular expressions.
115
+
116
+ ```python
117
+ TITLE = Entity(
118
+ name="TITLE",
119
+ pattern=r"(?:Mr|Mrs|Ms|Miss|Dr)\.?"
120
+ )
121
+ ```
122
+
123
+ Supply your entities when creating the translator.
124
+
125
+ ```python
126
+ translator = DSLTranslator(
127
+ entities=[
128
+ TITLE,
129
+ NAME,
130
+ POSTCODE,
131
+ ]
132
+ )
133
+ ```
134
+
135
+ Inside the DSL, entities are referenced using the `@` prefix.
136
+
137
+ ```text
138
+ @TITLE
139
+ @NAME
140
+ @POSTCODE
141
+ ```
142
+
143
+ If the DSL references an entity that was not supplied to `DSLTranslator`, an `UndefinedEntity` exception is raised.
144
+
145
+ ---
146
+
147
+ # Documentation
148
+
149
+ For the complete language reference, see **DSL_REFERENCE.md**.
150
+
151
+ The reference includes:
152
+
153
+ - Complete command reference
154
+ - DSL syntax
155
+ - Built-in helper tokens
156
+ - Conditional statements
157
+ - Examples
158
+ - Best practices
159
+ - Exception reference
160
+
161
+ ---
162
+
163
+ # Requirements
164
+
165
+ - Python 3.11+
166
+
167
+ ---
168
+
169
+ # Contributing
170
+
171
+ Contributions, bug reports and feature requests are welcome.
172
+
173
+ If you discover a bug or have an idea for improving **DocDSL**, please open an issue or submit a pull request.
174
+
175
+ ---
176
+
177
+ # License
178
+
179
+ Released under the MIT License.
docdsl-0.0.1/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # DocDSL
2
+
3
+ ![License](https://img.shields.io/static/v1?label=license&message=MIT&color=green)
4
+ ![Open Source](https://img.shields.io/static/v1?label=OpenSource&message=Yes&color=brightgreen)
5
+ ![Version](https://img.shields.io/static/v1?label=version&message=v.0.0.1&color=blue)
6
+ ![Status](https://img.shields.io/badge/status-alpha-yellow.svg)
7
+
8
+
9
+ A declarative DSL for extracting structured information from OCR output, PDFs, reports, and other semi-structured documents.
10
+
11
+ Instead of writing large, difficult-to-maintain regular expressions, **DocDSL** lets you describe **what** to extract using a simple, readable language while keeping extraction rules separate from your application code.
12
+
13
+ ---
14
+
15
+ # Installation
16
+
17
+ ```bash
18
+ pip install docdsl
19
+ ```
20
+
21
+ ---
22
+
23
+ # Quick Start
24
+
25
+ ## 1. Define your entities
26
+
27
+ Entities are reusable regular expressions that can be referenced throughout your DSL.
28
+
29
+ ```python
30
+ from docdsl import DSLTranslator, Entity
31
+
32
+ NAME = Entity(
33
+ name="NAME",
34
+ pattern=r"[A-Za-z ,.'-]+"
35
+ )
36
+
37
+ POSTCODE = Entity(
38
+ name="POSTCODE",
39
+ pattern=r"[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}"
40
+ )
41
+ ```
42
+
43
+ ## 2. Create a translator
44
+
45
+ ```python
46
+ translator = DSLTranslator(
47
+ entities=[
48
+ NAME,
49
+ POSTCODE,
50
+ ]
51
+ )
52
+ ```
53
+
54
+ ## 3. Write your extraction rules
55
+
56
+ ```python
57
+ dsl = """
58
+ FIND "Name:";
59
+ SKIP UNTIL NEWLINE;
60
+ CAPTURE TARGET [@NAME];
61
+ """
62
+ ```
63
+
64
+ ## 4. Generate the regular expression
65
+
66
+ ```python
67
+ pattern = translator.translate(dsl)
68
+ ```
69
+
70
+ Your DSL is translated into a regular expression that can be used as part of your document extraction pipeline.
71
+
72
+ ---
73
+
74
+ # Features
75
+
76
+ - Declarative, English-like extraction language
77
+ - Reusable named entities
78
+ - Readable extraction rules
79
+ - Conditional extraction using `IF`
80
+ - Multi-line capture
81
+ - Capture between delimiters
82
+ - Built-in helper tokens
83
+ - Friendly syntax and validation errors
84
+ - Exact error locations with line and column information
85
+
86
+ ---
87
+
88
+ # Entities
89
+
90
+ Entities represent reusable regular expressions.
91
+
92
+ ```python
93
+ TITLE = Entity(
94
+ name="TITLE",
95
+ pattern=r"(?:Mr|Mrs|Ms|Miss|Dr)\.?"
96
+ )
97
+ ```
98
+
99
+ Supply your entities when creating the translator.
100
+
101
+ ```python
102
+ translator = DSLTranslator(
103
+ entities=[
104
+ TITLE,
105
+ NAME,
106
+ POSTCODE,
107
+ ]
108
+ )
109
+ ```
110
+
111
+ Inside the DSL, entities are referenced using the `@` prefix.
112
+
113
+ ```text
114
+ @TITLE
115
+ @NAME
116
+ @POSTCODE
117
+ ```
118
+
119
+ If the DSL references an entity that was not supplied to `DSLTranslator`, an `UndefinedEntity` exception is raised.
120
+
121
+ ---
122
+
123
+ # Documentation
124
+
125
+ For the complete language reference, see **DSL_REFERENCE.md**.
126
+
127
+ The reference includes:
128
+
129
+ - Complete command reference
130
+ - DSL syntax
131
+ - Built-in helper tokens
132
+ - Conditional statements
133
+ - Examples
134
+ - Best practices
135
+ - Exception reference
136
+
137
+ ---
138
+
139
+ # Requirements
140
+
141
+ - Python 3.11+
142
+
143
+ ---
144
+
145
+ # Contributing
146
+
147
+ Contributions, bug reports and feature requests are welcome.
148
+
149
+ If you discover a bug or have an idea for improving **DocDSL**, please open an issue or submit a pull request.
150
+
151
+ ---
152
+
153
+ # License
154
+
155
+ Released under the MIT License.
@@ -0,0 +1,19 @@
1
+ """
2
+ File: __init__.py
3
+ Project: Docdsl
4
+ File Created: Wed 15 Jul 2026 08:07:46 (Shine Jayakumar)
5
+ Author: Shine Jayakumar (shinejayakumar@yahoo.com)
6
+ -----
7
+ Last Modified: Wed 15 Jul 2026 08:07:46 (Shine Jayakumar)
8
+ Modified By: Shine Jayakumar (shinejayakumar@yahoo.com)
9
+ -----
10
+ Copyright (c) 2026 Shine Jayakumar
11
+ SPDX-License-Identifier: MIT
12
+
13
+ Licensed under the MIT License.
14
+ See the LICENSE file in the project root for the full license text.
15
+ """
16
+
17
+ __version__ = "0.0.1"
18
+
19
+ from .translator import DSLTranslator, Entity
@@ -0,0 +1,338 @@
1
+ """
2
+ File: commands.py
3
+ Project: Docdsl
4
+ File Created: Wed 15 Jul 2026 08:07:54 (Shine Jayakumar)
5
+ Author: Shine Jayakumar (shinejayakumar@yahoo.com)
6
+ -----
7
+ Last Modified: Wed 15 Jul 2026 08:07:54 (Shine Jayakumar)
8
+ Modified By: Shine Jayakumar (shinejayakumar@yahoo.com)
9
+ -----
10
+ Copyright (c) 2026 Shine Jayakumar
11
+ SPDX-License-Identifier: MIT
12
+
13
+ Licensed under the MIT License.
14
+ See the LICENSE file in the project root for the full license text.
15
+ """
16
+
17
+ import re
18
+ from dataclasses import dataclass, field
19
+ from .dsl_builtins import *
20
+ from .text_to_regex import TextToRegex
21
+
22
+
23
+ @dataclass
24
+ class Entity:
25
+ parent: object
26
+ name: str
27
+
28
+ @property
29
+ def group_name(self):
30
+ return f"{self.name[1:]}"
31
+
32
+ def to_regex(self):
33
+ return f"__{self.name[1:]}__"
34
+
35
+
36
+ @dataclass
37
+ class Command:
38
+ parent: object
39
+
40
+
41
+ @dataclass
42
+ class Find(Command):
43
+ items: list = None
44
+
45
+ def to_regex(self):
46
+ pat_parts = [item.to_regex() for item in self.items]
47
+ pattern = "|".join(pat_parts)
48
+ pattern = f"(?:{pattern})" if len(self.items) > 1 else pattern
49
+ return pattern
50
+
51
+
52
+ @dataclass
53
+ class FindItem(Command):
54
+
55
+ text: str = ""
56
+ ignorecase: bool = False
57
+
58
+ def to_regex(self):
59
+ pat = TextToRegex().get_regex(self.text)
60
+ pat = f"(?i:{pat})" if self.ignorecase else pat
61
+ return pat
62
+
63
+
64
+ @dataclass
65
+ class Skip(Command):
66
+
67
+ position: str = ""
68
+ until: str = ""
69
+ ignorecase: bool = False
70
+
71
+ def to_regex(self):
72
+ if self.position:
73
+ return self.position.to_regex()
74
+
75
+ if self.until:
76
+ until = re.escape(self.until)
77
+ until = f"(?i:{until})" if self.ignorecase else until
78
+ pat = rf".*?{until}"
79
+ return pat
80
+
81
+
82
+ @dataclass
83
+ class SkipLines(Command):
84
+
85
+ until: str
86
+
87
+ def to_regex(self):
88
+ until = re.escape(self.until)
89
+ pat = rf"[\s\S]*?{until}"
90
+ return pat
91
+
92
+
93
+ @dataclass
94
+ class SkipNLines(Command):
95
+
96
+ lines: int
97
+
98
+ def to_regex(self):
99
+ pat = rf"(?:.*[\r\n]){{{self.lines}}}"
100
+ return pat
101
+
102
+
103
+ #@dataclass
104
+ #class Capture(Command):
105
+ #
106
+ # items: list = None
107
+ # until: str = ""
108
+ # position: Position = None
109
+ #
110
+ # def to_regex(self):
111
+ # target_count = sum([bool(item.target) for item in self.items])
112
+ # if not target_count:
113
+ # raise Exception("CAPTURE requires at least one target entity")
114
+ # if target_count > 1:
115
+ # raise Exception("CAPTURE cannot have more than one target entity")
116
+ #
117
+ # target = self.items[-1]
118
+ # pat_parts = [item.to_regex() for item in self.items[:-1]]
119
+ # target_pat = target.target.to_regex()
120
+ # group_name = target.target.entity.group_name
121
+ # pat_parts.append(f"(?P<{group_name}>{target_pat})")
122
+ # if self.until:
123
+ # until_pat = TextToRegex().get_regex(self.until)
124
+ # pat_parts.append(until_pat)
125
+ # elif self.position:
126
+ # pos = self.position.to_regex()
127
+ # pat_parts.append(pos)
128
+ # return "".join(pat_parts)
129
+
130
+ #def __repr__(self):
131
+ # return (
132
+ # "Capture(items=[\n"
133
+ # f"{self.items}\n]\n,"
134
+ # f"until={self.until},\n"
135
+ # f"position={self.position}\n"
136
+ # ")"
137
+ # )
138
+ @dataclass
139
+ class TargetEntity(Command):
140
+
141
+ entity: Entity
142
+
143
+ def to_regex(self):
144
+ return self.entity.to_regex()
145
+
146
+
147
+
148
+ @dataclass
149
+ class Capture(Command):
150
+
151
+ items: list = None
152
+ target: TargetEntity = None
153
+ until: str = ""
154
+ position: Position = None
155
+
156
+ def to_regex(self):
157
+ pat_parts = [item.to_regex() for item in self.items]
158
+ target_pat = self.target.entity.to_regex()
159
+ group_name = self.target.entity.group_name
160
+ pat_parts.append(f"(?P<{group_name}>{target_pat})")
161
+ if self.until:
162
+ until_pat = TextToRegex().get_regex(self.until)
163
+ pat_parts.append(until_pat)
164
+ elif self.position:
165
+ pos = self.position.to_regex()
166
+ pat_parts.append(pos)
167
+ return "".join(pat_parts)
168
+
169
+
170
+
171
+ #@dataclass
172
+ #class CaptureItem(Command):
173
+ #
174
+ # optional: bool = False
175
+ # builtin: Builtin=None
176
+ # entity: str = None
177
+ # target: TargetEntity = None
178
+ #
179
+ # def to_regex(self):
180
+ # if self.builtin:
181
+ # pat = self.builtin.to_regex()
182
+ # pat = f"{pat}?" if self.optional else pat
183
+ # return pat
184
+ #
185
+ # if self.entity:
186
+ # pat = self.entity.to_regex()
187
+ # pat = f"{pat}?" if self.optional else pat
188
+ # return pat
189
+ #
190
+ # if self.target:
191
+ # return self.target.entity.to_regex()
192
+
193
+ @dataclass
194
+ class CaptureItem(Command):
195
+
196
+ optional: bool = False
197
+ builtin: Builtin=None
198
+ entity: str = None
199
+
200
+ def to_regex(self):
201
+ if self.builtin:
202
+ pat = self.builtin.to_regex()
203
+ pat = f"{pat}?" if self.optional else pat
204
+ return pat
205
+
206
+ if self.entity:
207
+ pat = self.entity.to_regex()
208
+ pat = f"{pat}?" if self.optional else pat
209
+ return pat
210
+
211
+
212
+ @dataclass
213
+ class CaptureNLines(Command):
214
+
215
+ lines: int
216
+ target: TargetEntity
217
+
218
+ def to_regex(self):
219
+ group_name = self.target.entity.group_name
220
+ #pat = rf"(?P<{entity}>(?:.*[\r\n]){{{self.lines}}})"
221
+ pat = rf"(?P<{group_name}>(?:.*\r?\n){{{self.lines}}})"
222
+ return pat
223
+
224
+
225
+ @dataclass
226
+ class CaptureLines(Command):
227
+
228
+ until: str
229
+ target: TargetEntity
230
+ ignorecase: bool = False
231
+
232
+ def to_regex(self):
233
+ group_name = self.target.entity.group_name
234
+ between = AcrossLines(None, None).to_regex()
235
+ until = TextToRegex().get_regex(self.until)
236
+ until = f"(?i:{until})" if self.ignorecase else until
237
+ pat = rf"(?P<{group_name}>{between}){until}"
238
+ return pat
239
+
240
+
241
+ @dataclass
242
+ class CaptureBetween(Command):
243
+
244
+ start: str
245
+ end: str
246
+ target: TargetEntity
247
+ scope: CaptureLinesScope = None
248
+
249
+ def to_regex(self):
250
+ group_name = self.target.entity.group_name
251
+ start = self.start.to_regex()
252
+ end = self.end.to_regex()
253
+ between = (
254
+ AcrossLines(None, None).to_regex() if not self.scope
255
+ else self.scope.to_regex()
256
+ )
257
+ pat = rf"{start}(?P<{group_name}>{between}){end}"
258
+ return pat
259
+
260
+
261
+ @dataclass
262
+ class CaptureBetweenItem(Command):
263
+
264
+ text: str
265
+ ignorecase: bool = False
266
+
267
+ def to_regex(self):
268
+ pat = TextToRegex().get_regex(self.text)
269
+ pat = f"(?i:{pat})" if self.ignorecase else pat
270
+ return pat
271
+
272
+
273
+ @dataclass
274
+ class If(Command):
275
+
276
+ items: list = None
277
+ commands: list[Command] = None
278
+
279
+ def _get_item_pattern(self) -> str:
280
+ pat_parts = [item.to_regex() for item in self.items]
281
+ pat = "|".join(pat_parts)
282
+ pat = f"({pat})" if len(pat_parts) > 1 else pat
283
+ return pat
284
+
285
+ def _get_command_pattern(self) -> str:
286
+ pat_parts = [cmd.to_regex() for cmd in self.commands]
287
+ pat = "".join(pat_parts)
288
+ return pat
289
+
290
+ def to_regex(self):
291
+ #pat_parts = [item.to_regex() for item in self.items]
292
+ #pat = "|".join(pat_parts)
293
+ #pat = f"({pat})" if len(pat_parts) > 1 else pat
294
+
295
+ #pat += self.action.to_regex()
296
+ pat = self._get_item_pattern()
297
+ pat += self._get_command_pattern()
298
+ pat = f"(?:{pat})?"
299
+ return pat
300
+
301
+ #for item in self.items:
302
+ # pat = self.item.to_regex()
303
+ #pat = ""
304
+ #if self.keyword:
305
+ # kwd_pattern = TextToRegex().get_regex(self.keyword)
306
+ # pat += kwd_pattern
307
+ #elif self.entity:
308
+ # ent = f"__{self.entity}__"
309
+ # pat += ent
310
+ #action_pattern = self.action.to_regex()
311
+ #pat += action_pattern
312
+ #pat = f"(?:{pat})?"
313
+ #return pat
314
+
315
+
316
+
317
+ @dataclass
318
+ class IfItem(Command):
319
+
320
+ text: str = ""
321
+ entity: Entity = None
322
+ ignorecase: bool = False
323
+
324
+ def to_regex(self):
325
+ if self.text:
326
+ pat = TextToRegex().get_regex(self.text)
327
+ pat = f"(?i:{pat})" if self.ignorecase else pat
328
+ return pat
329
+
330
+ # for entity
331
+ pat = self.entity.to_regex()
332
+ pat = f"(?i:{pat})" if self.ignorecase else pat
333
+ return pat
334
+
335
+
336
+
337
+ if __name__ == "__main__":
338
+ pass