repo-parser 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.
- repo_parser-0.0.1/LICENSE +21 -0
- repo_parser-0.0.1/PKG-INFO +37 -0
- repo_parser-0.0.1/README.md +26 -0
- repo_parser-0.0.1/pyproject.toml +18 -0
- repo_parser-0.0.1/repo_parser/__init__.py +11 -0
- repo_parser-0.0.1/repo_parser/filesystem.py +70 -0
- repo_parser-0.0.1/repo_parser/processor.py +25 -0
- repo_parser-0.0.1/repo_parser/resource.py +89 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-2024 William Lachance
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: repo-parser
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A library for extracting metadata out of a source repository.
|
|
5
|
+
Author-email: William Lachance <wlach@protonmail.com>
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Requires-Dist: GitPython
|
|
9
|
+
Project-URL: Home, https://github.com/wlach/repo-parser
|
|
10
|
+
|
|
11
|
+
# repo-parser
|
|
12
|
+
|
|
13
|
+
This is a set of python scripts and tools for extracting metadata and structure
|
|
14
|
+
out of a monorepo, for the purposes of generating a service registry, unified documentation, or other types of data out of the contents.
|
|
15
|
+
|
|
16
|
+
It should be considered an experiment, rather than production software. It is inspired by my experiences in the software industry and being frustrated with current solutions being some combination of:
|
|
17
|
+
|
|
18
|
+
- Expensive
|
|
19
|
+
- Time consuming to configure and maintain
|
|
20
|
+
- Requiring the specification of redundant metadata (which is bound to get out of date)
|
|
21
|
+
|
|
22
|
+
Assumptions:
|
|
23
|
+
|
|
24
|
+
- The files you want to process fit into memory
|
|
25
|
+
- You don't care about history
|
|
26
|
+
|
|
27
|
+
You can see a demo of a documentation site generated from this repo at:
|
|
28
|
+
|
|
29
|
+
https://repo-parser-demo.netlify.app/
|
|
30
|
+
|
|
31
|
+
## Local development
|
|
32
|
+
|
|
33
|
+
You can experiment with the local demo by running `make example`.
|
|
34
|
+
It should live-reload as you make changes in the `example/repo` directory.
|
|
35
|
+
|
|
36
|
+
For other tasks, look at the (very simple) `Makefile` in the root directory.
|
|
37
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# repo-parser
|
|
2
|
+
|
|
3
|
+
This is a set of python scripts and tools for extracting metadata and structure
|
|
4
|
+
out of a monorepo, for the purposes of generating a service registry, unified documentation, or other types of data out of the contents.
|
|
5
|
+
|
|
6
|
+
It should be considered an experiment, rather than production software. It is inspired by my experiences in the software industry and being frustrated with current solutions being some combination of:
|
|
7
|
+
|
|
8
|
+
- Expensive
|
|
9
|
+
- Time consuming to configure and maintain
|
|
10
|
+
- Requiring the specification of redundant metadata (which is bound to get out of date)
|
|
11
|
+
|
|
12
|
+
Assumptions:
|
|
13
|
+
|
|
14
|
+
- The files you want to process fit into memory
|
|
15
|
+
- You don't care about history
|
|
16
|
+
|
|
17
|
+
You can see a demo of a documentation site generated from this repo at:
|
|
18
|
+
|
|
19
|
+
https://repo-parser-demo.netlify.app/
|
|
20
|
+
|
|
21
|
+
## Local development
|
|
22
|
+
|
|
23
|
+
You can experiment with the local demo by running `make example`.
|
|
24
|
+
It should live-reload as you make changes in the `example/repo` directory.
|
|
25
|
+
|
|
26
|
+
For other tasks, look at the (very simple) `Makefile` in the root directory.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "repo-parser"
|
|
7
|
+
authors = [{ name = "William Lachance", email = "wlach@protonmail.com" }]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = { file = "LICENSE" }
|
|
10
|
+
classifiers = ["License :: OSI Approved :: MIT License"]
|
|
11
|
+
dynamic = ["version", "description"]
|
|
12
|
+
dependencies = ["GitPython"]
|
|
13
|
+
|
|
14
|
+
[project.urls]
|
|
15
|
+
Home = "https://github.com/wlach/repo-parser"
|
|
16
|
+
|
|
17
|
+
[tool.ruff.lint]
|
|
18
|
+
select = ["E4", "E7", "E9", "F", "B", "I"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A library for extracting metadata out of a source repository.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.0.1"
|
|
6
|
+
|
|
7
|
+
from .filesystem import scan
|
|
8
|
+
from .processor import DEFAULT_PROCESSORS
|
|
9
|
+
from .resource import Resource, get_resources
|
|
10
|
+
|
|
11
|
+
__all__ = ["scan", "get_resources", "Resource", "DEFAULT_PROCESSORS"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import PurePath
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
import git
|
|
7
|
+
|
|
8
|
+
from .processor import Processor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class File:
|
|
13
|
+
name: str
|
|
14
|
+
src_path: PurePath
|
|
15
|
+
content: Optional[str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Dir:
|
|
20
|
+
path: pathlib.Path
|
|
21
|
+
files: List[File]
|
|
22
|
+
dirs: List["Dir"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _scan(path: pathlib.Path, processors: List[Processor], repo: git.Repo) -> Dir:
|
|
26
|
+
dir = Dir(path=pathlib.PurePath(path), files=[], dirs=[])
|
|
27
|
+
|
|
28
|
+
entries = [entry for entry in path.iterdir()]
|
|
29
|
+
if not entries:
|
|
30
|
+
return dir
|
|
31
|
+
# FIXME: At some point we should only ignore .git in the root directory
|
|
32
|
+
ignored = set(repo.ignored(*[entry.name for entry in entries]) + [".git"])
|
|
33
|
+
|
|
34
|
+
for entry in entries:
|
|
35
|
+
if entry.name in ignored:
|
|
36
|
+
continue
|
|
37
|
+
elif entry.is_file():
|
|
38
|
+
for processor in processors:
|
|
39
|
+
if processor.pattern.search(str(entry)):
|
|
40
|
+
content = entry.read_text() if processor.read_content else None
|
|
41
|
+
dir.files.append(
|
|
42
|
+
File(
|
|
43
|
+
name=entry.name,
|
|
44
|
+
src_path=(path / entry.name),
|
|
45
|
+
content=content,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
elif entry.is_dir():
|
|
49
|
+
dir.dirs.append(_scan(path / entry.name, processors, repo))
|
|
50
|
+
|
|
51
|
+
return dir
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def scan(path: pathlib.Path, processors: List[Processor]) -> Dir:
|
|
56
|
+
"""
|
|
57
|
+
Scans a GitHub repository for files and subdirectories, returning a data
|
|
58
|
+
structure representing the directory tree.
|
|
59
|
+
|
|
60
|
+
Takes in a list of processors to figure out which files should be included
|
|
61
|
+
in the returned tree.
|
|
62
|
+
|
|
63
|
+
Under the hood, this uses the `git` library to scan the repository, skipping
|
|
64
|
+
files in .gitignore.
|
|
65
|
+
|
|
66
|
+
This step does not do any post-processing of the files, though their
|
|
67
|
+
content is read in if one of their processors requires it.
|
|
68
|
+
"""
|
|
69
|
+
repo = git.Repo(path)
|
|
70
|
+
return _scan(path, processors, repo)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import frontmatter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Processor:
|
|
9
|
+
pattern: re.Pattern
|
|
10
|
+
process: callable
|
|
11
|
+
read_content: bool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _parse_markdown(file_contents: str):
|
|
15
|
+
metadata, _ = frontmatter.parse(file_contents)
|
|
16
|
+
|
|
17
|
+
filetype = metadata.get("type", "file")
|
|
18
|
+
metadata.pop("type", None) # remove "type" from the metadata
|
|
19
|
+
|
|
20
|
+
return filetype, metadata, file_contents
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEFAULT_PROCESSORS = [
|
|
24
|
+
Processor(re.compile("\.md$"), _parse_markdown, True),
|
|
25
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import PurePath
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from .filesystem import Dir
|
|
6
|
+
from .processor import Processor
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Resource:
|
|
11
|
+
name: str
|
|
12
|
+
src_path: PurePath
|
|
13
|
+
path: PurePath
|
|
14
|
+
type: str
|
|
15
|
+
metadata: dict
|
|
16
|
+
content: Optional[str]
|
|
17
|
+
children: List["Resource"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_resources(
|
|
21
|
+
dir: Dir, parent_path: PurePath, processors: List[Processor]
|
|
22
|
+
) -> List[Resource]:
|
|
23
|
+
child_resources: List[Resource] = []
|
|
24
|
+
dir_resource: Optional[Resource] = None
|
|
25
|
+
|
|
26
|
+
# do a first pass to see if we can find a "dir_resource"
|
|
27
|
+
for file in dir.files:
|
|
28
|
+
for processor in processors:
|
|
29
|
+
if processor.pattern.search(file.name):
|
|
30
|
+
filetype, metadata, content = processor.process(file.content)
|
|
31
|
+
if filetype != "file":
|
|
32
|
+
if dir_resource is None:
|
|
33
|
+
dir_resource = Resource(
|
|
34
|
+
name=dir.path.name,
|
|
35
|
+
src_path=dir.path,
|
|
36
|
+
path=PurePath(),
|
|
37
|
+
type=filetype,
|
|
38
|
+
metadata=metadata,
|
|
39
|
+
content=None,
|
|
40
|
+
children=[],
|
|
41
|
+
)
|
|
42
|
+
# parent path is now the dir_resource
|
|
43
|
+
parent_path = dir_resource.path
|
|
44
|
+
else:
|
|
45
|
+
# a previous processor picked this up, augment the metadata
|
|
46
|
+
# FIXME: I think we want to be able to override the name as well
|
|
47
|
+
dir_resource.metadata.update(metadata)
|
|
48
|
+
|
|
49
|
+
# do a second pass to find all the child resources, placing
|
|
50
|
+
# their paths relative to the parent
|
|
51
|
+
for file in dir.files:
|
|
52
|
+
if processor.pattern.search(file.name):
|
|
53
|
+
child_resources.append(
|
|
54
|
+
Resource(
|
|
55
|
+
name=file.name,
|
|
56
|
+
path=parent_path / file.name,
|
|
57
|
+
src_path=file.src_path,
|
|
58
|
+
type="file",
|
|
59
|
+
metadata=metadata,
|
|
60
|
+
content=file.content,
|
|
61
|
+
children=[],
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
for subdir in dir.dirs:
|
|
66
|
+
child_resources.extend(
|
|
67
|
+
_get_resources(subdir, parent_path / subdir.path.name, processors)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# if this directory did not define a new resource, append any newly
|
|
71
|
+
# found resources to the parent
|
|
72
|
+
if dir_resource is None:
|
|
73
|
+
return child_resources
|
|
74
|
+
|
|
75
|
+
# otherwise, add the child resources to the dir_resource, and return that
|
|
76
|
+
dir_resource.children = child_resources
|
|
77
|
+
return [dir_resource]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_resources(dir: Dir, processors: List[Processor]) -> Resource:
|
|
81
|
+
return Resource(
|
|
82
|
+
name=dir.path.name,
|
|
83
|
+
path=PurePath(),
|
|
84
|
+
src_path=dir.path,
|
|
85
|
+
type="repo",
|
|
86
|
+
metadata={},
|
|
87
|
+
children=_get_resources(dir, PurePath(), processors),
|
|
88
|
+
content=None,
|
|
89
|
+
)
|