• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Qiskit / ecosystem / 28786091494

06 Jul 2026 10:48AM UTC coverage: 67.08%. First build
28786091494

Pull #1223

github

web-flow
Merge 6863e5209 into 94b4e8664
Pull Request #1223: license normalization

60 of 111 new or added lines in 12 files covered. (54.05%)

1677 of 2500 relevant lines covered (67.08%)

0.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

28.57
/ecosystem/github.py
1
# This code is part of Qiskit.
2
#
3
# (C) Copyright IBM 2023.
4
#
5
# This code is licensed under the Apache License, Version 2.0. You may
6
# obtain a copy of this license in the LICENSE.txt file in the root directory
7
# of this source tree or at https://www.apache.org/licenses/LICENSE-2.0.
8
#
9
# Any modifications or derivative works of this code must retain this
10
# copyright notice, and modified files need to carry a notice indicating
11
# that they have been altered from the originals.
12

13
"""GitHub section."""
1✔
14

15
from re import match
1✔
16
from functools import reduce
1✔
17
from jsonpath import findall
1✔
18

19
from .license import License
1✔
20
from .serializable import JsonSerializable, parse_date
1✔
21
from .error_handling import EcosystemError, logger
1✔
22
from .request import (
1✔
23
    request_json,
24
    parse_github_package_ids,
25
    parse_github_dependants,
26
    parse_github_contributors_sidebar,
27
    URL,
28
)
29

30

31
class GitHubData(JsonSerializable):
1✔
32
    """
33
    The GitHub data related to a project
34
    """
35

36
    dict_keys = [
1✔
37
        "url",
38
        "owner",
39
        "repo",
40
        "tree",
41
        "stars",
42
        "homepage",
43
        "license",
44
        "description",
45
        "estimated_contributors",
46
        "total_dependent_repositories",
47
        "total_dependent_packages",
48
        "private",
49
        "archived",
50
        "disabled",
51
        "last_commit",
52
        "last_activity",
53
    ]
54
    aliases = {
1✔
55
        "stars": "stargazers_count",
56
        "last_commit": "pushed_at",
57
        "url": "html_url",
58
    }
59
    json_types = {
1✔
60
        "homepage": lambda x: x or None,
61
        "private": lambda x: x or None,
62
        "archived": lambda x: x or None,
63
        "disabled": lambda x: x or None,
64
        "description": lambda x: x[:131] + "..." if len(str(x)) > 135 else x,
65
        "pushed_at": parse_date,
66
    }
67
    reduce = {}
1✔
68

69
    def __init__(self, owner: str, repo: str, tree: str = None, **kwargs):
1✔
70
        self.owner = owner
×
71
        self.repo = repo
×
72
        self.tree = tree
×
73
        self._kwargs = kwargs or {}
×
74
        self._json_repo = None
×
75
        self._json_events = None
×
76
        self._json_package_ids = None
×
77
        self._json_dependants = None
×
78
        self._json_contributors_sidebar = None
×
79

80
    def to_dict(self, keys=None) -> dict:
1✔
NEW
81
        return super().to_dict(keys=keys or GitHubData.dict_keys)
×
82

83
    @classmethod
1✔
84
    def from_dict(cls, dictionary: dict):
1✔
NEW
85
        if "license" in dictionary and dictionary["license"] is not None:
×
NEW
86
            dictionary["license"] = License(dictionary["license"], where="github")
×
NEW
87
        return super().from_dict(dictionary)
×
88

89
    @classmethod
1✔
90
    def from_url(cls, github_project_url: URL):
1✔
91
        """
92
        Builds a GitHubSection from an url. Returns None
93
        if the given url is not a GitHub url
94
        """
95
        if "github.com" not in github_project_url.hostname:
×
96
            # github_project_url is not a GitHub URL
97
            return None
×
98

99
        tree_path = None
×
100
        url_path = github_project_url.path.replace("github.com", "")
×
101
        if "/tree/" in url_path:
×
102
            new_path, tree_path = url_path.split("/tree/")
×
103
            logger.debug(
×
104
                "%s includes a branch or a subdirectories: %s | %s",
105
                url_path,
106
                new_path,
107
                tree_path,
108
            )
109
            url_path = new_path
×
110
        try:
×
111
            owner, repo = [
×
112
                c for c in url_path.split("/") if match(r"^[A-Za-z0-9_.-]+$", c)
113
            ]
114
        except ValueError as exc:
×
115
            raise EcosystemError(f"invalid GitHub url: {github_project_url}") from exc
×
116

117
        return GitHubData(owner=owner, repo=repo, tree=tree_path)
×
118

119
    def update_json(self):
1✔
120
        """
121
        Fetches remote data from:
122
          - api.github.com/repos/{self.owner}/{self.repo}
123
          - github.com/{self.owner}/{self.repo}/network/dependents
124
          - github.com/{self.owner}/{self.repo}
125
          - api.github.com/networks/{self.owner}/{self.repo}/events
126

127
        """
128
        self._json_repo = request_json(f"api.github.com/repos/{self.owner}/{self.repo}")
×
129
        self._json_events = request_json(
×
130
            f"api.github.com/networks/{self.owner}/{self.repo}/events"
131
        )
132
        self._json_contributors_sidebar = request_json(
×
133
            f"github.com/{self.owner}/{self.repo}/contributors_list?deferred=true",
134
            parser=parse_github_contributors_sidebar,
135
        )
136
        self._json_package_ids = request_json(
×
137
            f"github.com/{self.owner}/{self.repo}/network/dependents?dependent_type=REPOSITORY",
138
            parser=parse_github_package_ids,
139
        )
140
        self._json_package_ids = {
×
141
            v: k
142
            for v, k in self._json_package_ids.items()
143
            if isinstance(v, str) and not v.startswith("__")
144
        }
145
        self._json_dependants = {}
×
146
        for package, package_id in self._json_package_ids.items():
×
147
            try:
×
148
                self._json_dependants[package] = request_json(
×
149
                    f"github.com/{self.owner}/{self.repo}/network/dependents?"
150
                    f"dependent_type=REPOSITORY&package_id={package_id}",
151
                    parser=parse_github_dependants,
152
                )
153
            except EcosystemError:
×
154
                logger.warning("json_dependants could not be updated")
×
155

156
    def __getattr__(self, item):
1✔
157
        if self._json_repo:
×
158
            if item in GitHubData.aliases:
×
159
                item = GitHubData.aliases[item]
×
160

161
            json_elements = findall(item, self._json_repo)
×
162
            if item in GitHubData.json_types:
×
163
                json_elements = [GitHubData.json_types[item](e) for e in json_elements]
×
164

165
            if len(json_elements) == 1:
×
166
                return json_elements[0]
×
167

168
            if len(json_elements) >= 2:
×
169
                return reduce(GitHubData.reduce[item], json_elements)
×
170

171
            raise AttributeError(
×
172
                f"'{type(self).__name__}' object has no " f"attribute '{item}'"
173
            )
174

175
        if item in self._kwargs:
×
176
            return self._kwargs[item]
×
177

178
        raise AttributeError(
×
179
            f"'{type(self).__name__}' object has no attribute '{item}'"
180
        )
181

182
    def update_owner_repo(self):
1✔
183
        """
184
        Updates GitHub page when the repo was moved or renamed
185
        """
186
        if self._json_repo is None:
×
187
            self.update_json()
×
188
        owner = self._json_repo["owner"]["login"]
×
189
        repo = self._json_repo["name"]
×
190
        if self.owner != owner or self.repo != repo:
×
191
            logger.info("%s/%s moved to %s/%s", self.owner, self.repo, owner, repo)
×
192
            self.owner = owner
×
193
            self.repo = repo
×
194

195
    def dependants(self, refresh=False):
1✔
196
        """get the dependant data from (cached) JSON"""
197
        if refresh:
×
198
            self.update_json()
×
199
        return self._json_dependants
×
200

201
    def contributors_sidebar_data(self, refresh=False):
1✔
202
        """get the front page data from (cached) JSON"""
203
        if refresh:
×
204
            self.update_json()
×
205
        return self._json_contributors_sidebar
×
206

207
    @property
1✔
208
    def estimated_contributors(self):
1✔
209
        """..."""
210
        if self.contributors_sidebar_data():
×
211
            return self.contributors_sidebar_data()["estimated_contributors"]
×
212
        return self._kwargs.get("estimated_contributors")
×
213

214
    @property
1✔
215
    def total_dependent_repositories(self):
1✔
216
        """Sum of repository dependants"""
217
        if self.dependants():
×
218
            return sum(r.get("repositories", 0) for r in self.dependants().values())
×
219
        return self._kwargs.get("total_dependent_repositories")
×
220

221
    @property
1✔
222
    def total_dependent_packages(self):
1✔
223
        """Sum of package dependants"""
224
        if self.dependants():
×
225
            return sum(r.get("packages", 0) for r in self.dependants().values())
×
226
        return self._kwargs.get("total_dependent_packages")
×
227

228
    @property
1✔
229
    def last_activity(self):
1✔
230
        """The creation of the last event"""
231
        if self._json_events and self._json_events["data"]:
×
232
            return parse_date(self._json_events["data"][0]["created_at"])
×
233
        return self._kwargs.get("last_activity")
×
234

235
    @property
1✔
236
    def license(self):
1✔
237
        """The creation of the last event"""
NEW
238
        json_license_name = self._json_repo.get("license", {}).get("name")
×
NEW
239
        if json_license_name:
×
NEW
240
            return License(json_license_name, "github")
×
NEW
241
        if "license" in self._kwargs:
×
NEW
242
            if isinstance(self._kwargs["license"], License):
×
NEW
243
                return self._kwargs["license"]
×
NEW
244
            return License(self._kwargs["license"], "github")
×
NEW
245
        return None
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc