grimoirelab-chronicler 0.0.1rc2__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.
@@ -0,0 +1,3 @@
1
+ Santiago Dueñas <sduenas@bitergia.com>
2
+ Jose Javier Merchante <jjmerchante@bitergia.com>
3
+
File without changes
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.1
2
+ Name: grimoirelab-chronicler
3
+ Version: 0.0.1rc2
4
+ Summary: Generator of GrimoireLab events using Perceval data.
5
+ Home-page: https://chaoss.github.io/grimoirelab/
6
+ License: GPL-3.0+
7
+ Keywords: software analytics,events,grimoirelab
8
+ Author: GrimoireLab Developers
9
+ Requires-Python: >=3.11,<4.0
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Information Technology
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Topic :: Software Development
18
+ Requires-Dist: click (>=8.1.7,<9.0.0)
19
+ Requires-Dist: cloudevents (>=1.10.1,<2.0.0)
20
+ Requires-Dist: coverage (>=7.2.3,<8.0.0)
21
+ Project-URL: Repository, https://github.com/chaoss/grimoirelab-chronicler
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Chronicler
25
+
26
+ Tool and library to convert Perceval data into events.
27
+
28
+ ## Usage
29
+
30
+ ```text
31
+ $ chronicler --help
32
+ Usage: chronicler [OPTIONS] DATASOURCE
33
+
34
+ Generates GrimoireLab events from the items obtained by Perceval.
35
+
36
+ The chronicler is a command line tool and a library that converts items
37
+ generated by Perceval into events. The tool reads these events from the
38
+ standard input or from a given file.
39
+
40
+ To run it, you will have to give the type of items the chronicler is
41
+ receiving with DATASOURCE argument (e.g. git, github, gitlab).
42
+
43
+ Output is produced using JSON format. Use the option <json_line>
44
+ to generate JSON object per line.
45
+
46
+ Options:
47
+ --input FILENAME File with perceval items
48
+ --output FILENAME File where events will be written
49
+ --json-line Produce a JSON line for each output item
50
+ --version Show the version and exit.
51
+ --help Show this message and exit.
52
+ ```
53
+
54
+ You can connect the **Chronicler** with **Perceval** to generate events.
55
+
56
+ ```sh
57
+ perceval git --json-line https://example.com | chronicler git
58
+ ```
59
+
60
+ ## Installation
61
+
62
+ There are several methods to install this tool.
63
+
64
+ ### pip
65
+
66
+ ```sh
67
+ pip install grimoirelab-chronicler
68
+ ```
69
+
70
+ ### poetry (source code)
71
+
72
+ We use [poetry](https://python-poetry.org/) for dependency management and
73
+ packaging. You can install **chronicler** and its dependencies on an
74
+ isolated environment provided by **poetry**.
75
+
76
+ ```sh
77
+ poetry install
78
+ poetry shell
79
+ ```
80
+
81
+ ## Contributing
82
+
83
+ Chronicler is part of the GrimoireLab project. Please read its
84
+ [Contributing Guidelines](https://github.com/chaoss/grimoirelab/blob/main/CONTRIBUTING.md)
85
+ for more information.
86
+
@@ -0,0 +1,62 @@
1
+ # Chronicler
2
+
3
+ Tool and library to convert Perceval data into events.
4
+
5
+ ## Usage
6
+
7
+ ```text
8
+ $ chronicler --help
9
+ Usage: chronicler [OPTIONS] DATASOURCE
10
+
11
+ Generates GrimoireLab events from the items obtained by Perceval.
12
+
13
+ The chronicler is a command line tool and a library that converts items
14
+ generated by Perceval into events. The tool reads these events from the
15
+ standard input or from a given file.
16
+
17
+ To run it, you will have to give the type of items the chronicler is
18
+ receiving with DATASOURCE argument (e.g. git, github, gitlab).
19
+
20
+ Output is produced using JSON format. Use the option <json_line>
21
+ to generate JSON object per line.
22
+
23
+ Options:
24
+ --input FILENAME File with perceval items
25
+ --output FILENAME File where events will be written
26
+ --json-line Produce a JSON line for each output item
27
+ --version Show the version and exit.
28
+ --help Show this message and exit.
29
+ ```
30
+
31
+ You can connect the **Chronicler** with **Perceval** to generate events.
32
+
33
+ ```sh
34
+ perceval git --json-line https://example.com | chronicler git
35
+ ```
36
+
37
+ ## Installation
38
+
39
+ There are several methods to install this tool.
40
+
41
+ ### pip
42
+
43
+ ```sh
44
+ pip install grimoirelab-chronicler
45
+ ```
46
+
47
+ ### poetry (source code)
48
+
49
+ We use [poetry](https://python-poetry.org/) for dependency management and
50
+ packaging. You can install **chronicler** and its dependencies on an
51
+ isolated environment provided by **poetry**.
52
+
53
+ ```sh
54
+ poetry install
55
+ poetry shell
56
+ ```
57
+
58
+ ## Contributing
59
+
60
+ Chronicler is part of the GrimoireLab project. Please read its
61
+ [Contributing Guidelines](https://github.com/chaoss/grimoirelab/blob/main/CONTRIBUTING.md)
62
+ for more information.
File without changes
@@ -0,0 +1,2 @@
1
+ # File auto-generated by semverup on 2025-01-16 09:15:00.503408
2
+ __version__ = "0.0.1-rc.2"
@@ -0,0 +1,76 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) GrimoireLab Developers
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ import json
20
+ import sys
21
+
22
+ import click
23
+ import cloudevents.conversion
24
+
25
+ from ._version import __version__
26
+ from .eventizer import eventize
27
+
28
+
29
+ @click.command
30
+ @click.option(
31
+ "--input",
32
+ help="File with perceval items",
33
+ type=click.File("r"),
34
+ default=sys.stdin
35
+ )
36
+ @click.option(
37
+ "--output",
38
+ help="File where events will be written",
39
+ type=click.File("w"),
40
+ default=sys.stdout
41
+ )
42
+ @click.option(
43
+ "--json-line",
44
+ help="Produce a JSON line for each output item",
45
+ is_flag=True,
46
+ show_default=True,
47
+ default=False
48
+ )
49
+ @click.argument('datasource')
50
+ @click.version_option(__version__, message="%(prog)s %(version)s")
51
+ def chronicler(datasource, input, output, json_line):
52
+ """Generates GrimoireLab events from the items fetched by Perceval.
53
+
54
+ The chronicler is a command line tool and a library that converts
55
+ items generated by Perceval into events. The tool reads
56
+ these events from the standard input or from a given file.
57
+
58
+ To run it, you will have to give the type of items the chronicler
59
+ is receiving with DATASOURCE argument (e.g. git, github, gitlab).
60
+
61
+ Output is produced using JSON format. Use the option <json_line>
62
+ to generate JSON object per line.
63
+ """
64
+ def _read_input(input):
65
+ for line in input:
66
+ yield json.loads(line)
67
+
68
+ for item in eventize(datasource, _read_input(input)):
69
+ if json_line:
70
+ obj = json.dumps(cloudevents.conversion.to_dict(item),
71
+ separators=(',', ':'), sort_keys=True)
72
+ else:
73
+ obj = json.dumps(cloudevents.conversion.to_dict(item),
74
+ indent=4, sort_keys=True)
75
+ output.write(obj)
76
+ output.write('\n')
@@ -0,0 +1,159 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) GrimoireLab Developers
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ import importlib
20
+ import pkgutil
21
+ import os
22
+
23
+ from collections.abc import Iterator, Generator
24
+ from typing import Any
25
+
26
+ from cloudevents.http import CloudEvent
27
+
28
+
29
+ class Eventizer:
30
+ """Abstract class to eventize data.
31
+
32
+ Events will be generated calling to the `eventize` method.
33
+ This method will take items produced by `perceval` and
34
+ will convert them in GrimoireLab events.
35
+
36
+ The class can only process items of one type. In other words,
37
+ `git` items can't be mixed with `github` items. However,
38
+ the same eventizer can process different categories of
39
+ the same type. A `github` eventizer would process `issues`,
40
+ `pull requests`, etc.
41
+
42
+ To create your own eventizer, sub-class this class by implementing
43
+ the method `eventize_item`. This method should have the
44
+ logic to given a perceval item, produce the events associated to
45
+ that item.
46
+ """
47
+ def eventize(self, raw_items: Iterator[dict[str, Any]]) -> Generator[CloudEvent]:
48
+ """Generate GrimoireLab events.
49
+
50
+ Produce events from the given list of perceval items.
51
+ The items must be of the same type.
52
+ """
53
+ for raw_item in raw_items:
54
+ yield from self.eventize_item(raw_item)
55
+
56
+ def eventize_item(self, raw_item: dict[str, Any]) -> list[CloudEvent]:
57
+ """Eventize a item."""
58
+ raise NotImplementedError
59
+
60
+
61
+ def eventize(name: str, raw_items: Iterator[dict[str, Any]]) -> Generator[CloudEvent]:
62
+ """Eventize data of a given type.
63
+
64
+ Handy function to produce events from a set of perceval items
65
+ of given type.
66
+ """
67
+ top_package_name = os.environ.get('CHRONICLER_EVENTIZERS',
68
+ 'chronicler.events')
69
+
70
+ eventizers = _find_eventizers(top_package_name)
71
+
72
+ try:
73
+ eventizer = eventizers[name]()
74
+ except KeyError:
75
+ raise ValueError(f"Unknown eventizer '{name}'")
76
+
77
+ yield from eventizer.eventize(raw_items)
78
+
79
+
80
+ def _find_eventizers(top_package_name: str) -> dict[str, Eventizer]:
81
+ """Find available eventizers.
82
+
83
+ Look for the `Eventizer` classes under `top_package`
84
+ and its sub-packages. When `top_package` defines a namespace,
85
+ classes under that same namespace will be found too.
86
+
87
+ :param top_package: package storing eventizer classes
88
+
89
+ :returns: a dict with `Eventizer`
90
+ """
91
+ top_package = importlib.import_module(top_package_name)
92
+
93
+ candidates = pkgutil.walk_packages(top_package.__path__,
94
+ prefix=top_package.__name__ + '.')
95
+
96
+ modules = [name for _, name, is_pkg in candidates if not is_pkg]
97
+
98
+ return _import_eventizers(modules)
99
+
100
+
101
+ def _import_eventizers(modules):
102
+ for module in modules:
103
+ importlib.import_module(module)
104
+
105
+ klasses = _find_classes(Eventizer, modules)
106
+
107
+ eventizers = {name: kls for name, kls in klasses}
108
+
109
+ return eventizers
110
+
111
+
112
+ def _find_classes(parent, modules):
113
+ parents = parent.__subclasses__()
114
+
115
+ while parents:
116
+ kls = parents.pop()
117
+
118
+ m = kls.__module__
119
+
120
+ if m not in modules:
121
+ continue
122
+
123
+ name = m.split('.')[-1]
124
+ parents.extend(kls.__subclasses__())
125
+
126
+ yield name, kls
127
+
128
+
129
+ def uuid(*args):
130
+ """Generate a UUID based on the given parameters.
131
+
132
+ The UUID will be the SHA1 of the concatenation of the values
133
+ from the list. The separator between these values is ':'.
134
+ Each value must be a non-empty string, otherwise, the function
135
+ will raise an exception.
136
+
137
+ :param *args: list of arguments used to generate the UUID
138
+
139
+ :returns: a universal unique identifier
140
+
141
+ :raises ValueError: when anyone of the values is not a string,
142
+ is empty or `None`.
143
+ """
144
+ import hashlib
145
+
146
+ def check_value(v):
147
+ if not isinstance(v, str):
148
+ raise ValueError("%s value is not a string instance" % str(v))
149
+ elif not v:
150
+ raise ValueError("value cannot be None or empty")
151
+ else:
152
+ return v
153
+
154
+ s = ':'.join(map(check_value, args))
155
+
156
+ sha1 = hashlib.sha1(s.encode('utf-8', errors='surrogateescape'))
157
+ uuid_sha1 = sha1.hexdigest()
158
+
159
+ return uuid_sha1
@@ -0,0 +1,21 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) GrimoireLab Developers
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ from ..._version import __version__
20
+
21
+ __version__ = __version__
@@ -0,0 +1,141 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) GrimoireLab Developers
4
+ #
5
+ # This program is free software; you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation; either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ #
18
+
19
+ from typing import Any
20
+
21
+ from cloudevents.http import CloudEvent
22
+
23
+ from ...eventizer import Eventizer, uuid
24
+
25
+
26
+ GIT_EVENT_COMMIT = "org.grimoirelab.events.git.commit"
27
+ GIT_EVENT_MERGE_COMMIT = "org.grimoirelab.events.git.merge"
28
+ GIT_EVENT_ACTION_ADDED = "org.grimoirelab.events.git.file.added"
29
+ GIT_EVENT_ACTION_MODIFIED = "org.grimoirelab.events.git.file.modified"
30
+ GIT_EVENT_ACTION_DELETED = "org.grimoirelab.events.git.file.deleted"
31
+ GIT_EVENT_ACTION_REPLACED = "org.grimoirelab.events.git.file.replaced"
32
+ GIT_EVENT_ACTION_COPIED = "org.grimoirelab.events.git.file.copied"
33
+ GIT_EVENT_ACTION_TYPE_CHANGED = "org.grimoirelab.events.git.file.typechanged"
34
+
35
+
36
+ class GitEventizer(Eventizer):
37
+ """Eventize git commits"""
38
+
39
+ def eventize_item(self, raw_item: dict[str, Any]) -> list[dict[str, Any]]:
40
+ events = []
41
+
42
+ item_uuid = raw_item.get('uuid', None)
43
+
44
+ if not item_uuid:
45
+ raise ValueError("'uuid' attribute not found on item.")
46
+ if raw_item['backend_name'].lower() != 'git':
47
+ raise ValueError(f"Item {item_uuid} is not a 'git' item.")
48
+ if raw_item['category'] != 'commit':
49
+ raise ValueError(f"Invalid category '{raw_item['category']}' for '{item_uuid}' item.")
50
+
51
+ if 'Merge' in raw_item['data']:
52
+ event_type = GIT_EVENT_MERGE_COMMIT
53
+ else:
54
+ event_type = GIT_EVENT_COMMIT
55
+
56
+ attributes = {
57
+ "id": item_uuid,
58
+ "type": event_type,
59
+ "source": raw_item['origin'],
60
+ "time": raw_item['updated_on'],
61
+ }
62
+
63
+ event = CloudEvent(attributes, raw_item['data'])
64
+ events.append(event)
65
+
66
+ action_events = self._eventize_commit_actions(event,
67
+ raw_item['data']['files'])
68
+
69
+ events.extend(action_events)
70
+
71
+ return events
72
+
73
+ def _eventize_commit_actions(self, parent_event: CloudEvent, raw_files_data):
74
+
75
+ events = []
76
+
77
+ for file_data in raw_files_data:
78
+ actions = file_data.get('action', None)
79
+
80
+ if not actions and parent_event['type'] == GIT_EVENT_COMMIT:
81
+ raise ValueError(f"No action for commit event {parent_event['id']}")
82
+ elif not actions:
83
+ continue
84
+
85
+ if parent_event['type'] == GIT_EVENT_COMMIT:
86
+ action_event = self._process_action(parent_event['source'],
87
+ parent_event['time'],
88
+ parent_event['id'], actions, file_data)
89
+ events.append(action_event)
90
+ else:
91
+ prev_merge_action = None
92
+ for action in actions:
93
+ if action == prev_merge_action:
94
+ continue
95
+
96
+ action_event = self._process_action(parent_event['source'],
97
+ parent_event['time'],
98
+ parent_event['id'], action, file_data)
99
+ events.append(action_event)
100
+ prev_merge_action = action
101
+ return events
102
+
103
+ def _process_action(self, source, time, event_uuid, action, file_data):
104
+ if action == 'A':
105
+ event_type = GIT_EVENT_ACTION_ADDED
106
+ elif action == 'M':
107
+ event_type = GIT_EVENT_ACTION_MODIFIED
108
+ elif action == 'D':
109
+ event_type = GIT_EVENT_ACTION_DELETED
110
+ elif action.startswith('R'):
111
+ event_type = GIT_EVENT_ACTION_REPLACED
112
+ elif action.startswith('C'):
113
+ event_type = GIT_EVENT_ACTION_COPIED
114
+ elif action.startswith('T'):
115
+ event_type = GIT_EVENT_ACTION_TYPE_CHANGED
116
+ else:
117
+ raise ValueError(f"No valid action: {action}")
118
+
119
+ event_id = uuid(event_uuid, file_data['file'], action)
120
+
121
+ data = {
122
+ "filename": file_data['file'],
123
+ "modes": file_data['modes'],
124
+ "indexes": file_data['indexes'],
125
+ "similarity": action[1:] if action in ('R', 'C') else None,
126
+ "new_filename": file_data.get('newfile', None),
127
+ "added_lines": file_data.get('added', None),
128
+ "deleted_lines": file_data.get('removed', None)
129
+ }
130
+
131
+ attributes = {
132
+ "id": event_id,
133
+ "linked_event": event_uuid,
134
+ "type": event_type,
135
+ "source": source,
136
+ "time": time,
137
+ }
138
+
139
+ event = CloudEvent(attributes, data)
140
+
141
+ return event
@@ -0,0 +1,56 @@
1
+ [tool.poetry]
2
+ name = "grimoirelab-chronicler"
3
+ version = "0.0.1-rc.2"
4
+ description = "Generator of GrimoireLab events using Perceval data."
5
+ authors = [
6
+ "GrimoireLab Developers"
7
+ ]
8
+ license = "GPL-3.0+"
9
+
10
+ readme = "README.md"
11
+
12
+ homepage = "https://chaoss.github.io/grimoirelab/"
13
+ repository = "https://github.com/chaoss/grimoirelab-chronicler"
14
+
15
+ keywords = [
16
+ "software analytics",
17
+ "events",
18
+ "grimoirelab"
19
+ ]
20
+
21
+ classifiers = [
22
+ "Development Status :: 3 - Alpha",
23
+ "Intended Audience :: Developers",
24
+ "Intended Audience :: Information Technology",
25
+ "Intended Audience :: Science/Research",
26
+ "Topic :: Software Development",
27
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
28
+ "Programming Language :: Python :: 3"
29
+ ]
30
+
31
+ packages = [
32
+ { include = "chronicler"},
33
+ ]
34
+
35
+ include = [
36
+ { path = "AUTHORS", format = "sdist" },
37
+ { path = "NEWS", format = "sdist" },
38
+ { path = "README.md", format = "sdist" },
39
+ ]
40
+
41
+ [tool.poetry.scripts]
42
+ chronicler = "chronicler.chronicler:chronicler"
43
+
44
+ [tool.poetry.dependencies]
45
+ python = "^3.11"
46
+
47
+ click = "^8.1.7"
48
+ cloudevents = "^1.10.1"
49
+ coverage = "^7.2.3"
50
+
51
+ [tool.poetry.group.dev.dependencies]
52
+ flake8 = "^7.1.1"
53
+
54
+ [build-system]
55
+ requires = ["poetry-core"]
56
+ build-backend = "poetry.core.masonry.api"