Changeset View
Standalone View
swh/web/misc/coverage.py
# Copyright (C) 2018-2019 The Software Heritage developers | # Copyright (C) 2018-2021 The Software Heritage developers | |||||||||||
# See the AUTHORS file at the top-level directory of this distribution | # See the AUTHORS file at the top-level directory of this distribution | |||||||||||
# License: GNU Affero General Public License version 3, or any later version | # License: GNU Affero General Public License version 3, or any later version | |||||||||||
# See top-level LICENSE file for more information | # See top-level LICENSE file for more information | |||||||||||
from collections import Counter, defaultdict | ||||||||||||
from functools import lru_cache | ||||||||||||
from typing import Dict, List, Tuple | ||||||||||||
from urllib.parse import urlparse | ||||||||||||
from django.conf.urls import url | from django.conf.urls import url | |||||||||||
from django.shortcuts import render | from django.shortcuts import render | |||||||||||
from django.views.decorators.clickjacking import xframe_options_exempt | from django.views.decorators.clickjacking import xframe_options_exempt | |||||||||||
from swh.web.config import get_config | from swh.scheduler.model import SchedulerMetrics | |||||||||||
from swh.web.common.utils import get_deposits_list | ||||||||||||
from swh.web.config import scheduler | ||||||||||||
_swh_arch_overview_doc = ( | ||||||||||||
"https://docs.softwareheritage.org/devel/architecture/overview.html" | ||||||||||||
) | ||||||||||||
# Current coverage list of the archive | # Current coverage list of the archive in a high level overview fashion, | |||||||||||
# TODO: Retrieve that list dynamically instead of hardcoding it | # categorized as follow: | |||||||||||
_code_providers = [ | # - listed origins: origins discovered using a swh lister | |||||||||||
{ | # - legacy: origins where public hosting service has closed | |||||||||||
"provider_id": "bitbucket", | # - deposited: origins coming from swh-deposit | |||||||||||
"provider_url": "https://bitbucket.org/", | # - miscellaneous: other origin types | |||||||||||
"provider_logo": "img/logos/bitbucket.png", | # TODO: Store that list in a database table somewhere (swh-scheduler, swh-storage ?) | |||||||||||
ardumont: or swh-web's? | ||||||||||||
Done Inline ActionsThose info could be needed in other swh components so I would not put that in swh-web db. anlambert: Those info could be needed in other swh components so I would not put that in swh-web db. | ||||||||||||
Not Done Inline Actionssounds fair. ardumont: sounds fair. | ||||||||||||
"provider_info": "public repositories from Bitbucket " | # and retrieve it dynamically | |||||||||||
"(continuously archived)", | _listed_origins = { | |||||||||||
"origin_url_regexp": "^https://bitbucket.org/", | "info": ( | |||||||||||
"origin_types": "repositories", | "These software origins get continuously discovered and archived using " | |||||||||||
}, | f'the <a href="{_swh_arch_overview_doc}#listers" target="_blank" ' | |||||||||||
{ | 'rel="noopener noreferrer">listers</a> implemented by Software Heritage.' | |||||||||||
"provider_id": "cran", | ), | |||||||||||
"provider_url": "https://cran.r-project.org/", | "origins": [ | |||||||||||
"provider_logo": "img/logos/cran.svg", | { | |||||||||||
"provider_info": "source packages from The Comprehensive R Archive " | "type": "bitbucket", | |||||||||||
"Network (continuously archived)", | "info_url": "https://bitbucket.org", | |||||||||||
"origin_url_regexp": "^https://cran.r-project.org/", | "info": "public repositories from Bitbucket", | |||||||||||
"origin_types": "packages", | "search_pattern": "https://bitbucket.org/", | |||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "debian", | ||||||||||||
"provider_url": "https://www.debian.org/", | ||||||||||||
"provider_logo": "img/logos/debian.png", | ||||||||||||
"provider_info": "source packages from the Debian distribution " | ||||||||||||
"(continuously archived)", | ||||||||||||
"origin_url_regexp": "^deb://", | ||||||||||||
"origin_types": "packages", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "framagit", | ||||||||||||
"provider_url": "https://framagit.org/", | ||||||||||||
"provider_logo": "img/logos/framagit.png", | ||||||||||||
"provider_info": "public repositories from Framagit " "(continuously archived)", | ||||||||||||
"origin_url_regexp": "^https://framagit.org/", | ||||||||||||
"origin_types": "repositories", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "github", | ||||||||||||
"provider_url": "https://github.com", | ||||||||||||
"provider_logo": "img/logos/github.png", | ||||||||||||
"provider_info": "public repositories from GitHub " "(continuously archived)", | ||||||||||||
"origin_url_regexp": "^https://github.com/", | ||||||||||||
"origin_types": "repositories", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "gitlab", | ||||||||||||
"provider_url": "https://gitlab.com", | ||||||||||||
"provider_logo": "img/logos/gitlab.svg", | ||||||||||||
"provider_info": "public repositories from GitLab " "(continuously archived)", | ||||||||||||
"origin_url_regexp": "^https://gitlab.com/", | ||||||||||||
"origin_types": "repositories", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "gitorious", | ||||||||||||
"provider_url": "https://gitorious.org/", | ||||||||||||
"provider_logo": "img/logos/gitorious.png", | ||||||||||||
"provider_info": "public repositories from the former Gitorious code " | ||||||||||||
"hosting service", | ||||||||||||
"origin_url_regexp": "^https://gitorious.org/", | ||||||||||||
"origin_types": "repositories", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "googlecode", | ||||||||||||
"provider_url": "https://code.google.com/archive/", | ||||||||||||
"provider_logo": "img/logos/googlecode.png", | ||||||||||||
"provider_info": "public repositories from the former Google Code " | ||||||||||||
"project hosting service", | ||||||||||||
"origin_url_regexp": "^http.*.googlecode.com/", | ||||||||||||
"origin_types": "repositories", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "gnu", | ||||||||||||
"provider_url": "https://www.gnu.org", | ||||||||||||
"provider_logo": "img/logos/gnu.png", | ||||||||||||
"provider_info": "releases from the GNU project (as of August 2015)", | ||||||||||||
"origin_url_regexp": "^rsync://ftp.gnu.org/", | ||||||||||||
"origin_types": "releases", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "guix", | ||||||||||||
"provider_url": "https://guix.gnu.org/", | ||||||||||||
"provider_logo": "img/logos/guix.svg", | ||||||||||||
"provider_info": "source code tarballs used to build the Guix package " | ||||||||||||
"collection", | ||||||||||||
"origin_url_regexp": "^https://guix.gnu.org/", | ||||||||||||
"origin_types": "tarballs", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "hal", | ||||||||||||
"provider_url": "https://hal.archives-ouvertes.fr/", | ||||||||||||
"provider_logo": "img/logos/hal.png", | ||||||||||||
"provider_info": "scientific software source code deposited in the " | ||||||||||||
"open archive HAL", | ||||||||||||
"origin_url_regexp": "^https://hal.archives-ouvertes.fr/", | ||||||||||||
"origin_types": "deposits", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "inria", | ||||||||||||
"provider_url": "https://gitlab.inria.fr", | ||||||||||||
"provider_logo": "img/logos/inria.jpg", | ||||||||||||
"provider_info": "public repositories from Inria GitLab " | ||||||||||||
"(continuously archived)", | ||||||||||||
"origin_url_regexp": "^https://gitlab.inria.fr/", | ||||||||||||
"origin_types": "repositories", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "ipol", | ||||||||||||
"provider_url": "https://www.ipol.im/", | ||||||||||||
"provider_logo": "img/logos/ipol.png", | ||||||||||||
"provider_info": "software artifacts associated to the articles " | ||||||||||||
"IPOL publishes", | ||||||||||||
"origin_url_regexp": "^https://doi.org/10.5201/ipol", | ||||||||||||
"origin_types": "tarballs", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "npm", | ||||||||||||
"provider_url": "https://www.npmjs.com/", | ||||||||||||
"provider_logo": "img/logos/npm.png", | ||||||||||||
"provider_info": "public packages from the package registry for " | ||||||||||||
"javascript (continuously archived)", | ||||||||||||
"origin_url_regexp": "^https://www.npmjs.com/", | ||||||||||||
"origin_types": "packages", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "nixos", | ||||||||||||
"provider_url": "https://nixos.org/", | ||||||||||||
"provider_logo": "img/logos/nixos.png", | ||||||||||||
"provider_info": "source code tarballs used to build the Nix package " | ||||||||||||
"collection", | ||||||||||||
"origin_url_regexp": "^https://nix-community.github.io/nixpkgs-swh", | ||||||||||||
"origin_types": "tarballs", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"provider_id": "pypi", | ||||||||||||
"provider_url": "https://pypi.org", | ||||||||||||
"provider_logo": "img/logos/pypi.svg", | ||||||||||||
"provider_info": "source packages from the Python Packaging Index " | ||||||||||||
"(continuously archived)", | ||||||||||||
"origin_url_regexp": "^https://pypi.org/", | ||||||||||||
"origin_types": "packages", | ||||||||||||
}, | }, | |||||||||||
{ | ||||||||||||
"type": "cgit", | ||||||||||||
"info_url": "https://git.zx2c4.com/cgit/about", | ||||||||||||
"info": "public repositories from cgit instances", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "CRAN", | ||||||||||||
"info_url": "https://cran.r-project.org", | ||||||||||||
"info": "source packages from The Comprehensive R Archive Network", | ||||||||||||
"search_pattern": "https://cran.r-project.org/", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "debian", | ||||||||||||
"info_url": "https://www.debian.org", | ||||||||||||
"info": "source packages from the Debian distribution", | ||||||||||||
"search_pattern": "deb://", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "gitea", | ||||||||||||
"info_url": "https://gitea.io", | ||||||||||||
"info": "public repositories from Gitea instances", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "github", | ||||||||||||
"info_url": "https://github.com", | ||||||||||||
"info": "public repositories from GitHub", | ||||||||||||
"search_pattern": "https://github.com/", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "gitlab", | ||||||||||||
"info_url": "https://gitlab.com", | ||||||||||||
"info": "public repositories from multiple GitLab instances", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "GNU", | ||||||||||||
"info_url": "https://www.gnu.org", | ||||||||||||
"info": "releases from the GNU project (as of August 2015)", | ||||||||||||
"search_pattern": "gnu", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "launchpad", | ||||||||||||
Not Done Inline Actionsshould be like the other nixguix loader visit type is nixguix [1] That probably explains why we got so few in your screenshot (26) is too few to my taste ;) ardumont: should be like the other nixguix loader visit type is nixguix [1]
But those are complicated… | ||||||||||||
Done Inline ActionsI missed the nixguix loader configuartion for guix, correct origin counts is the number of branches in that origin, will adapt to retrieve it. anlambert: I missed the nixguix loader configuartion for guix, correct origin counts is the number of… | ||||||||||||
Not Done Inline Actionsright! ardumont: right! | ||||||||||||
"info_url": "https://launchpad.net", | ||||||||||||
"logo": "img/logos/launchpad.png", | ||||||||||||
"info": "public repositories from Launchpad", | ||||||||||||
"search_pattern": "https://git.launchpad.net/", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "npm", | ||||||||||||
"info_url": "https://www.npmjs.com", | ||||||||||||
"info": "public packages from the package registry for javascript", | ||||||||||||
"search_pattern": "https://www.npmjs.com", | ||||||||||||
Not Done Inline Actions
(proposal) ardumont: (proposal) | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "phabricator", | ||||||||||||
"info_url": "https://www.phacility.com/phabricator", | ||||||||||||
"info": "public repositories from multiple Phabricator instances", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
Not Done Inline Actionsi'd say nixpkgs here. ardumont: i'd say nixpkgs here. | ||||||||||||
Done Inline ActionsI use the type value to get associated png logo in static assets so I will keep nixos value here. anlambert: I use the type value to get associated png logo in static assets so I will keep nixos value… | ||||||||||||
Not Done Inline Actions
ack ardumont: > I use the type value to get associated png logo in static assets so I will keep nixos value… | ||||||||||||
Not Done Inline ActionsNixOS is the linux distribution (that uses nix and nix's dsl for everything). I understood nixpkgs as the superset of all packages (including the one we can install for nixos). ardumont: NixOS is the linux distribution (that uses nix and nix's dsl for everything).
Nixpkgs is the… | ||||||||||||
"type": "pypi", | ||||||||||||
"info_url": "https://pypi.org", | ||||||||||||
"info": "source packages from the Python Package Index", | ||||||||||||
"search_pattern": "https://www.npmjs.com/", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "sourceforge", | ||||||||||||
"info_url": "https://sourceforge.net", | ||||||||||||
"info": "public repositories from SourceForge", | ||||||||||||
"search_pattern": "code.sf.net", | ||||||||||||
}, | ||||||||||||
], | ||||||||||||
} | ||||||||||||
_legacy_origins = { | ||||||||||||
"info": ( | ||||||||||||
"Discontinued hosting services. Those origins have been archived " | ||||||||||||
"by Software Heritage." | ||||||||||||
), | ||||||||||||
"origins": [ | ||||||||||||
{ | ||||||||||||
"type": "gitorious", | ||||||||||||
"info_url": "https://en.wikipedia.org/wiki/Gitorious", | ||||||||||||
"info": ( | ||||||||||||
"public repositories from the former Gitorious code hosting service" | ||||||||||||
), | ||||||||||||
"visit_types": ["git"], | ||||||||||||
"search_pattern": "https://gitorious.org", | ||||||||||||
"count": "122,014", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "googlecode", | ||||||||||||
"info_url": "https://code.google.com/archive", | ||||||||||||
"info": ( | ||||||||||||
"public repositories from the former Google Code project " | ||||||||||||
"hosting service" | ||||||||||||
), | ||||||||||||
"visit_types": ["git", "hg", "svn"], | ||||||||||||
"search_pattern": "googlecode.com", | ||||||||||||
"count": "790,026", | ||||||||||||
}, | ||||||||||||
], | ||||||||||||
} | ||||||||||||
_deposited_origins = { | ||||||||||||
"info": ( | ||||||||||||
"These origins are directly pushed into the archive by trusted partners " | ||||||||||||
f'using the <a href="{_swh_arch_overview_doc}#deposit" target="_blank" ' | ||||||||||||
'rel="noopener noreferrer">deposit</a> service of Software Heritage.' | ||||||||||||
), | ||||||||||||
"origins": [ | ||||||||||||
{ | ||||||||||||
"type": "elife", | ||||||||||||
"info_url": "https://elifesciences.org", | ||||||||||||
"info": ( | ||||||||||||
"research software source code associated to the articles " | ||||||||||||
"eLife publishes" | ||||||||||||
), | ||||||||||||
"netloc": "elife.stencila.io", | ||||||||||||
"visit_types": ["deposit"], | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "hal", | ||||||||||||
"info_url": "https://hal.archives-ouvertes.fr", | ||||||||||||
"info": "scientific software source code deposited in the open archive HAL", | ||||||||||||
"visit_types": ["deposit"], | ||||||||||||
"netloc": "hal.archives-ouvertes.fr", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "ipol", | ||||||||||||
"info_url": "https://www.ipol.im", | ||||||||||||
"info": "software artifacts associated to the articles IPOL publishes", | ||||||||||||
"visit_types": ["deposit"], | ||||||||||||
"netloc": "doi.org/10.5201", | ||||||||||||
}, | ||||||||||||
], | ||||||||||||
} | ||||||||||||
_miscellaneous_origins = { | ||||||||||||
"info": ( | ||||||||||||
"These origins do not fall into the above categories but are important " | ||||||||||||
"to mention." | ||||||||||||
), | ||||||||||||
"origins": [ | ||||||||||||
{ | ||||||||||||
"type": "guix", | ||||||||||||
"info_url": "https://guix.gnu.org", | ||||||||||||
"info": "source code tarballs used to build the Guix package collection", | ||||||||||||
}, | ||||||||||||
{ | ||||||||||||
"type": "nixos", | ||||||||||||
"info_url": "https://nixos.org", | ||||||||||||
"info": "source code tarballs used to build the Nix package collection", | ||||||||||||
"visit_types": ["nixguix"], | ||||||||||||
}, | ||||||||||||
], | ||||||||||||
} | ||||||||||||
@lru_cache() | ||||||||||||
def _get_listers_metrics() -> Dict[str, List[Tuple[str, SchedulerMetrics]]]: | ||||||||||||
listers_metrics = defaultdict(list) | ||||||||||||
listers = scheduler().get_listers() | ||||||||||||
Done Inline ActionsThere should be a fallback when this fails (eg. because the scheduler isn't available, or not configured in the testing env) vlorentz: There should be a fallback when this fails (eg. because the scheduler isn't available, or not… | ||||||||||||
Done Inline ActionsI ensured that widget will still be displayed when metrics are not available, only counters info will be missing in that case. anlambert: I ensured that widget will still be displayed when metrics are not available, only counters… | ||||||||||||
scheduler_metrics = scheduler().get_metrics() | ||||||||||||
for lister in listers: | ||||||||||||
for metrics in filter(lambda m: m.lister_id == lister.id, scheduler_metrics): | ||||||||||||
listers_metrics[lister.name].append((lister.instance_name, metrics)) | ||||||||||||
return listers_metrics | ||||||||||||
@lru_cache() | ||||||||||||
def _get_deposits_netloc_counts() -> Counter: | ||||||||||||
def _process_origin_url(origin_url): | ||||||||||||
parsed_url = urlparse(origin_url) | ||||||||||||
netloc = parsed_url.netloc | ||||||||||||
# special treatment for doi.org netloc as it is not specific enough | ||||||||||||
# for origins mapping | ||||||||||||
if parsed_url.netloc == "doi.org": | ||||||||||||
netloc += "/" + parsed_url.path.split("/")[1] | ||||||||||||
return netloc | ||||||||||||
deposits = get_deposits_list() | ||||||||||||
netlocs = [ | ||||||||||||
_process_origin_url(d["origin_url"]) for d in deposits if d["status"] == "done" | ||||||||||||
] | ] | |||||||||||
return Counter(netlocs) | ||||||||||||
@xframe_options_exempt | @xframe_options_exempt | |||||||||||
Not Done Inline Actionsardumont: til [1]
[1] https://docs.djangoproject.com/en/3.2/ref/clickjacking/#setting-x-frame-options… | ||||||||||||
Done Inline ActionsOriginally that view was also integrated in an iframe on softwareheritage.org but this is no longer the case. anlambert: Originally that view was also integrated in an iframe on softwareheritage.org but this is no… | ||||||||||||
def _swh_coverage(request): | def _swh_coverage(request): | |||||||||||
count_origins = get_config()["coverage_count_origins"] | listers_metrics = _get_listers_metrics() | |||||||||||
for origins in _listed_origins["origins"]: | ||||||||||||
origins_type = origins["type"] | ||||||||||||
if origins_type not in listers_metrics: | ||||||||||||
continue | ||||||||||||
count = sum([d[1].origins_known for d in listers_metrics[origins_type]]) | ||||||||||||
count_never_visited = sum( | ||||||||||||
[d[1].origins_never_visited for d in listers_metrics[origins_type]] | ||||||||||||
) | ||||||||||||
Not Done Inline Actionshow weird!? ardumont: how weird!? | ||||||||||||
Done Inline ActionsYes, do not know what is wrong here but this what we currently have in scheduler_metrics table: softwareheritage-scheduler=> select name, instance_name, last_update, origins_known, origins_enabled, origins_never_visited, origins_with_pending_changes from listers inner join scheduler_metrics on id = lister_id order by name; name | instance_name | last_update | origins_known | origins_enabled | origins_never_visited | origins_with_pending_changes ---------------+------------------------------+-------------------------------+---------------+-----------------+-----------------------+------------------------------ CRAN | cran | 2021-07-13 12:14:07.919027+00 | 18292 | 18292 | 18292 | 0 anlambert: Yes, do not know what is wrong here but this what we currently have in scheduler_metrics table… | ||||||||||||
# CRAN origins are currently marked as not visited while they have been | ||||||||||||
if origins_type != "CRAN": | ||||||||||||
count -= count_never_visited | ||||||||||||
origins["count"] = f"{count:,}" | ||||||||||||
origins["instances"] = defaultdict(dict) | ||||||||||||
for d in listers_metrics[origins_type]: | ||||||||||||
origins["instances"][d[0]].update( | ||||||||||||
{d[1].visit_type: d[1].origins_known - d[1].origins_never_visited} | ||||||||||||
) | ||||||||||||
Not Done Inline Actions
That might pose an issue because long term, other deposit clients could also reference their origin through the doi site... The ipol specific doi is https://doi.org/10.5201/ so might be change the netloc entry to what the changes suggest? Or maybe it's yagni, let's consider this when the time comes? At least now you know ^ ;) ardumont: That might pose an issue because long term, other deposit clients could also reference their… | ||||||||||||
Done Inline ActionsI think the best way is to add a special processing for doi.org netloc and add the first path to it. anlambert: I think the best way is to add a special processing for doi.org netloc and add the first path… | ||||||||||||
origins["visit_types"] = list( | ||||||||||||
set(origins["instances"][d[0]].keys()) | ||||||||||||
| set(origins.get("visit_types", [])) | ||||||||||||
Not Done Inline Actions
ardumont: | ||||||||||||
Not Done Inline Actions
Scratch my previous suggestion change then ;) ardumont: Scratch my previous suggestion change then ;)
Given one of your last comment, then this method… | ||||||||||||
Done Inline Actionsalready done ;-) update incoming anlambert: already done ;-) update incoming | ||||||||||||
Not Done Inline Actions*thumbs up* ardumont: *thumbs up* | ||||||||||||
) | ||||||||||||
if origins_type == "CRAN": | ||||||||||||
origins["instances"]["cran"]["cran"] = origins["count"] | ||||||||||||
# defaultdict cannot be iterated in django template | ||||||||||||
origins["instances"] = dict(origins["instances"]) | ||||||||||||
deposits_counts = _get_deposits_netloc_counts() | ||||||||||||
for origins in _deposited_origins["origins"]: | ||||||||||||
if origins["netloc"] in deposits_counts: | ||||||||||||
origins["count"] = f"{deposits_counts[origins['netloc']]:,}" | ||||||||||||
return render( | return render( | |||||||||||
request, | request, | |||||||||||
"misc/coverage.html", | "misc/coverage.html", | |||||||||||
Not Done Inline Actionscrashes with KeyError: 'instances' in an empty docker instance. vlorentz: crashes with `KeyError: 'instances'` in an empty docker instance. | ||||||||||||
{"providers": _code_providers, "count_origins": count_origins}, | { | |||||||||||
"origins": { | ||||||||||||
"Listed origins": _listed_origins, | ||||||||||||
"Legacy origins": _legacy_origins, | ||||||||||||
"Deposited origins": _deposited_origins, | ||||||||||||
"Miscellaneous origins": _miscellaneous_origins, | ||||||||||||
} | ||||||||||||
}, | ||||||||||||
) | ) | |||||||||||
urlpatterns = [ | urlpatterns = [ | |||||||||||
url(r"^coverage/$", _swh_coverage, name="swh-coverage"), | url(r"^coverage/$", _swh_coverage, name="swh-coverage"), | |||||||||||
] | ] |
or swh-web's?