From 7c0dad159fd7a044647c2b2a4fb81c6c85f0f872 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 19:09:36 -0300 Subject: [PATCH 01/47] =?UTF-8?q?Moderniza=20a=20administra=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20tarefas=20peri=C3=B3dicas=20no=20Wagtail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Substitui a administração antiga pelos conjuntos de visualização nativos de snippets, preservando filtros, busca, execução individual e ações em massa. Protege as ações por permissão, mantém o aviso sobre o agendador e remove a dependência obsoleta. Inclui testes direcionados. --- config/settings/base.py | 2 - django_celery_beat/admin.py | 2 +- django_celery_beat/button_helper.py | 36 --- django_celery_beat/schedulers.py | 10 + .../periodic_task_index.html | 11 + django_celery_beat/tests/__init__.py | 0 .../tests/test_wagtail_admin.py | 59 ++++ django_celery_beat/utils.py | 13 - django_celery_beat/views.py | 45 +-- django_celery_beat/wagtail_hooks.py | 286 +++++++++--------- requirements/base.txt | 4 - 11 files changed, 253 insertions(+), 215 deletions(-) delete mode 100644 django_celery_beat/button_helper.py create mode 100644 django_celery_beat/templates/django_celery_beat/periodic_task_index.html create mode 100644 django_celery_beat/tests/__init__.py create mode 100644 django_celery_beat/tests/test_wagtail_admin.py diff --git a/config/settings/base.py b/config/settings/base.py index 63ffcd07..bc17f12f 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -89,10 +89,8 @@ "core.search_site", "wagtail.contrib.forms", "wagtail.contrib.redirects", - "wagtail_modeladmin", "wagtail.contrib.simple_translation", "wagtail_localize", - "wagtail_localize.modeladmin", "wagtail.embeds", "wagtail.sites", "wagtail.users", diff --git a/django_celery_beat/admin.py b/django_celery_beat/admin.py index e848e2e8..e3196e1b 100644 --- a/django_celery_beat/admin.py +++ b/django_celery_beat/admin.py @@ -18,7 +18,7 @@ PeriodicTasks, SolarSchedule, ) -from .utils import is_database_scheduler +from .schedulers import is_database_scheduler class TaskSelectWidget(Select): diff --git a/django_celery_beat/button_helper.py b/django_celery_beat/button_helper.py deleted file mode 100644 index 3515b68b..00000000 --- a/django_celery_beat/button_helper.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.urls import reverse -from django.utils.translation import gettext as _ -from wagtail_modeladmin.helpers import ButtonHelper - - -class PeriodicTaskHelper(ButtonHelper): - # Define classes for our button, here we can set an icon for example - run_button_classnames = [ - "button-small", - "icon", - ] - - def run_button(self, obj): - # Define a label for our button - text = _("Run") - return { - "url": reverse("django_celery_beat:task_run") + "?task_id=%s" % str(obj.id), - "label": text, - "classname": self.finalise_classname(self.run_button_classnames), - "title": text, - } - - def get_buttons_for_obj( - self, obj, exclude=None, classnames_add=None, classnames_exclude=None - ): - """ - This function is used to gather all available buttons. - We append our custom button to the btns list. - """ - btns = super().get_buttons_for_obj( - obj, exclude, classnames_add, classnames_exclude - ) - if "run" not in (exclude or []): - btns.append(self.run_button(obj)) - - return btns diff --git a/django_celery_beat/schedulers.py b/django_celery_beat/schedulers.py index ec2c581d..17b094d0 100644 --- a/django_celery_beat/schedulers.py +++ b/django_celery_beat/schedulers.py @@ -12,6 +12,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import close_old_connections, transaction from django.db.utils import DatabaseError, InterfaceError +from kombu.utils import symbol_by_name from kombu.utils.encoding import safe_repr, safe_str from kombu.utils.json import dumps, loads @@ -39,6 +40,15 @@ debug, info, warning = logger.debug, logger.info, logger.warning +def is_database_scheduler(scheduler): + if not scheduler: + return False + + return scheduler == "django" or issubclass( + symbol_by_name(scheduler), DatabaseScheduler + ) + + class ModelEntry(ScheduleEntry): """Scheduler entry taken from database row.""" diff --git a/django_celery_beat/templates/django_celery_beat/periodic_task_index.html b/django_celery_beat/templates/django_celery_beat/periodic_task_index.html new file mode 100644 index 00000000..0de9d663 --- /dev/null +++ b/django_celery_beat/templates/django_celery_beat/periodic_task_index.html @@ -0,0 +1,11 @@ +{% extends "wagtailsnippets/snippets/index.html" %} +{% load i18n %} + +{% block listing %} + {% if wrong_scheduler %} +
+ {% trans "Celery Beat is not configured to use the database scheduler." %} +
+ {% endif %} + {{ block.super }} +{% endblock %} diff --git a/django_celery_beat/tests/__init__.py b/django_celery_beat/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/django_celery_beat/tests/test_wagtail_admin.py b/django_celery_beat/tests/test_wagtail_admin.py new file mode 100644 index 00000000..8555e008 --- /dev/null +++ b/django_celery_beat/tests/test_wagtail_admin.py @@ -0,0 +1,59 @@ +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.test import TestCase +from django.urls import reverse + +from django_celery_beat.models import IntervalSchedule, PeriodicTask +from django_celery_beat.wagtail_hooks import ( + DisableTasksBulkAction, + EnableTasksBulkAction, + ToggleTasksBulkAction, +) + + +class PeriodicTaskWagtailAdminTests(TestCase): + def setUp(self): + self.user = get_user_model().objects.create_superuser( + username="admin", + email="admin@example.org", + password="password", + ) + interval = IntervalSchedule.objects.create( + every=1, + period=IntervalSchedule.MINUTES, + ) + self.task = PeriodicTask.objects.create( + name="Test task", + task="tests.task", + enabled=False, + interval=interval, + ) + + def test_snippet_listing_keeps_run_action(self): + self.client.force_login(self.user) + + response = self.client.get( + reverse("wagtailsnippets_django_celery_beat_periodictask:list") + ) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Test task") + self.assertContains(response, "Run") + + @patch("django_celery_beat.wagtail_hooks.PeriodicTasks.update_changed") + def test_state_bulk_actions_signal_scheduler(self, update_changed): + queryset = PeriodicTask.objects.filter(pk=self.task.pk) + + EnableTasksBulkAction.execute_action(queryset) + self.task.refresh_from_db() + self.assertTrue(self.task.enabled) + + ToggleTasksBulkAction.execute_action(queryset) + self.task.refresh_from_db() + self.assertFalse(self.task.enabled) + + DisableTasksBulkAction.execute_action(queryset) + self.task.refresh_from_db() + self.assertFalse(self.task.enabled) + self.assertEqual(update_changed.call_count, 3) diff --git a/django_celery_beat/utils.py b/django_celery_beat/utils.py index 3ca6ea07..7f304eb1 100644 --- a/django_celery_beat/utils.py +++ b/django_celery_beat/utils.py @@ -34,16 +34,3 @@ def now(): return now_localtime(timezone.now()) else: return timezone.now() - - -def is_database_scheduler(scheduler): - """Return true if Celery is configured to use the db scheduler.""" - if not scheduler: - return False - from kombu.utils import symbol_by_name - - from .schedulers import DatabaseScheduler - - return scheduler == "django" or issubclass( - symbol_by_name(scheduler), DatabaseScheduler - ) diff --git a/django_celery_beat/views.py b/django_celery_beat/views.py index 86327ec8..9747dcc7 100644 --- a/django_celery_beat/views.py +++ b/django_celery_beat/views.py @@ -2,6 +2,7 @@ import json from celery import current_app +from django.contrib.auth.decorators import permission_required from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils.translation import gettext as _ @@ -10,6 +11,28 @@ from django_celery_beat import models +def execute_task(periodic_task, user=None): + current_app.loader.import_default_modules() + task = current_app.tasks.get(periodic_task.task) + if task is None: + return False + + kwargs = json.loads(periodic_task.kwargs) + signature = inspect.signature(task.run) + if user and "user_id" in signature.parameters: + kwargs["user_id"] = user.id + + task.apply_async( + args=json.loads(periodic_task.args), + kwargs=kwargs, + queue=periodic_task.queue, + periodic_task_name=periodic_task.name, + ) + + return True + + +@permission_required("django_celery_beat.change_periodictask", raise_exception=True) def task_run(request): """ View funciton to run the task by PeriodicTask id. @@ -19,28 +42,14 @@ def task_run(request): p_task = get_object_or_404(models.PeriodicTask, pk=task_id) - current_app.loader.import_default_modules() - - task = current_app.tasks.get(p_task.task) - - if task is None: + if not execute_task(p_task, request.user): messages.error( request, _("Task '{0}' not found in the Celery registry.").format(p_task.task), ) - return redirect(request.META.get("HTTP_REFERER") or reverse("wagtailadmin_home")) - - kwargs = json.loads(p_task.kwargs) - sig = inspect.signature(task.run) - if "user_id" in sig.parameters: - kwargs["user_id"] = request.user.id - - task.apply_async( - args=json.loads(p_task.args), - kwargs=kwargs, - queue=p_task.queue, - periodic_task_name=p_task.name, - ) + return redirect( + request.META.get("HTTP_REFERER") or reverse("wagtailadmin_home") + ) messages.success(request, _("Task {0} was successfully run").format(p_task.name)) diff --git a/django_celery_beat/wagtail_hooks.py b/django_celery_beat/wagtail_hooks.py index e8c19948..a0dbb1c7 100644 --- a/django_celery_beat/wagtail_hooks.py +++ b/django_celery_beat/wagtail_hooks.py @@ -1,13 +1,17 @@ -from celery import current_app from django.conf import settings -from django.contrib import messages from django.db.models import Case, Value, When from django.template.defaultfilters import pluralize -from django.urls import include, path +from django.urls import include, path, reverse from django.utils.translation import gettext_lazy as _ -from kombu.utils.json import loads from wagtail import hooks -from wagtail_modeladmin.options import ModelAdmin, ModelAdminGroup, modeladmin_register +from wagtail.admin.widgets import Button +from wagtail.snippets.bulk_actions.snippet_bulk_action import SnippetBulkAction +from wagtail.snippets.models import register_snippet +from wagtail.snippets.views.snippets import ( + IndexView, + SnippetViewSet, + SnippetViewSetGroup, +) from config.menu import get_menu_order from django_celery_beat.models import ( @@ -18,19 +22,26 @@ PeriodicTasks, SolarSchedule, ) -from django_celery_beat.utils import is_database_scheduler +from django_celery_beat.schedulers import is_database_scheduler +from django_celery_beat.views import execute_task + -from .button_helper import PeriodicTaskHelper +class PeriodicTaskIndexView(IndexView): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + scheduler = getattr(settings, "CELERY_BEAT_SCHEDULER", None) + context["wrong_scheduler"] = not is_database_scheduler(scheduler) + return context -class PeriodicTaskAdmin(ModelAdmin): - """Admin-interface for periodic tasks.""" - button_helper_class = PeriodicTaskHelper +class PeriodicTaskSnippetViewSet(SnippetViewSet): model = PeriodicTask - menu_icon = "cog" - celery_app = current_app - date_hierarchy = "start_time" + icon = "cog" + menu_label = _("Periodic tasks") + menu_order = 100 + index_view_class = PeriodicTaskIndexView + index_template_name = "django_celery_beat/periodic_task_index.html" list_display = ( "__str__", "enabled", @@ -39,166 +50,159 @@ class PeriodicTaskAdmin(ModelAdmin): "last_run_at", "one_off", ) - list_filter = [ - "enabled", - "one_off", - "task", - ] - actions = ("enable_tasks", "disable_tasks", "toggle_tasks", "run_tasks") + list_filter = ("enabled", "one_off", "task") search_fields = ("name",) - def changelist_view(self, request, extra_context=None): - extra_context = extra_context or {} - scheduler = getattr(settings, "CELERYBEAT_SCHEDULER", None) - extra_context["wrong_scheduler"] = not is_database_scheduler(scheduler) - return super(PeriodicTaskAdmin, self).changelist_view(request, extra_context) - def get_queryset(self, request): - qs = super().get_queryset(request) - return qs.select_related("interval", "crontab", "solar", "clocked") - - def _message_user_about_update(self, request, rows_updated, verb): - """Send message about action to user. - `verb` should shortly describe what have changed (e.g. 'enabled'). - """ - self.message_user( - request, - _("{0} task{1} {2} successfully {3}").format( - rows_updated, - pluralize(rows_updated), - pluralize(rows_updated, _("was,were")), - verb, - ), + return self.model.objects.select_related( + "interval", "crontab", "solar", "clocked" ) - def enable_tasks(self, request, queryset): - rows_updated = queryset.update(enabled=True) - PeriodicTasks.update_changed() - self._message_user_about_update(request, rows_updated, "enabled") - enable_tasks.short_description = _("Enable selected tasks") +class CrontabScheduleSnippetViewSet(SnippetViewSet): + model = CrontabSchedule + icon = "date" + menu_label = _("Crontab schedules") + menu_order = 200 - def disable_tasks(self, request, queryset): - rows_updated = queryset.update(enabled=False, last_run_at=None) - PeriodicTasks.update_changed() - self._message_user_about_update(request, rows_updated, "disabled") - disable_tasks.short_description = _("Disable selected tasks") +class IntervalScheduleSnippetViewSet(SnippetViewSet): + model = IntervalSchedule + icon = "date" + menu_label = _("Interval schedules") + menu_order = 300 - def _toggle_tasks_activity(self, queryset): - return queryset.update( - enabled=Case( - When(enabled=True, then=Value(False)), - default=Value(True), - ) - ) - def toggle_tasks(self, request, queryset): - rows_updated = self._toggle_tasks_activity(queryset) - PeriodicTasks.update_changed() - self._message_user_about_update(request, rows_updated, "toggled") - - toggle_tasks.short_description = _("Toggle activity of selected tasks") - - def run_tasks(self, request, queryset): - self.celery_app.loader.import_default_modules() - tasks = [ - ( - self.celery_app.tasks.get(task.task), - loads(task.args), - loads(task.kwargs), - task.queue, - task.name, - ) - for task in queryset - ] +class ClockedScheduleSnippetViewSet(SnippetViewSet): + model = ClockedSchedule + icon = "time" + menu_label = _("Clocked schedules") + menu_order = 400 + list_display = ("clocked_time",) + form_fields = ("clocked_time",) - if any(t[0] is None for t in tasks): - for i, t in enumerate(tasks): - if t[0] is None: - break - # variable "i" will be set because list "tasks" is not empty - not_found_task_name = queryset[i].task +class SolarScheduleSnippetViewSet(SnippetViewSet): + model = SolarSchedule + icon = "date" + menu_label = _("Solar schedules") + menu_order = 500 - self.message_user( - request, - _('task "{0}" not found'.format(not_found_task_name)), - level=messages.ERROR, - ) - return - - task_ids = [ - task.apply_async( - args=args, - kwargs=kwargs, - queue=queue, - periodic_task_name=periodic_task_name, - ) - if queue and len(queue) - else task.apply_async( - args=args, kwargs=kwargs, periodic_task_name=periodic_task_name - ) - for task, args, kwargs, queue, periodic_task_name in tasks - ] - tasks_run = len(task_ids) - self.message_user( - request, - _("{0} task{1} {2} successfully run").format( - tasks_run, - pluralize(tasks_run), - pluralize(tasks_run, _("was,were")), - ), + +class TasksSnippetViewSetGroup(SnippetViewSetGroup): + menu_name = "tasks" + menu_label = _("Tasks") + menu_icon = "cogs" + menu_order = get_menu_order("tasks") + items = ( + PeriodicTaskSnippetViewSet, + CrontabScheduleSnippetViewSet, + IntervalScheduleSnippetViewSet, + ClockedScheduleSnippetViewSet, + SolarScheduleSnippetViewSet, + ) + + +register_snippet(TasksSnippetViewSetGroup) + + +class PeriodicTaskBulkAction(SnippetBulkAction): + template_name = "wagtailadmin/bulk_actions/confirmation/base.html" + models = [PeriodicTask] + + def check_perm(self, obj): + return self.request.user.has_perm("django_celery_beat.change_periodictask") + + def get_success_message(self, num_parent_objects, num_child_objects): + return _("{0} task{1} {2} successfully {3}").format( + num_parent_objects, + pluralize(num_parent_objects), + pluralize(num_parent_objects, _("was,were")), + self.success_verb, ) - run_tasks.short_description = _("Run selected tasks") +@hooks.register("register_bulk_action") +class EnableTasksBulkAction(PeriodicTaskBulkAction): + display_name = _("Enable") + aria_label = _("Enable selected tasks") + action_type = "enable_periodic_tasks" + success_verb = _("enabled") -class ClockedScheduleAdmin(ModelAdmin): - """Admin-interface for clocked schedules.""" + @classmethod + def execute_action(cls, objects, **kwargs): + rows_updated = objects.update(enabled=True) + PeriodicTasks.update_changed() - menu_icon = "time" - model = ClockedSchedule + return rows_updated, 0 - fields = ("clocked_time",) - list_display = ("clocked_time",) +@hooks.register("register_bulk_action") +class DisableTasksBulkAction(PeriodicTaskBulkAction): + display_name = _("Disable") + aria_label = _("Disable selected tasks") + action_type = "disable_periodic_tasks" + success_verb = _("disabled") -class IntervalScheduleAdmin(ModelAdmin): - """Admin-interface for clocked schedules.""" + @classmethod + def execute_action(cls, objects, **kwargs): + rows_updated = objects.update(enabled=False, last_run_at=None) + PeriodicTasks.update_changed() - menu_icon = "date" - model = IntervalSchedule + return rows_updated, 0 -class CrontabScheduleAdmin(ModelAdmin): - """Admin-interface for clocked schedules.""" +@hooks.register("register_bulk_action") +class ToggleTasksBulkAction(PeriodicTaskBulkAction): + display_name = _("Toggle") + aria_label = _("Toggle selected tasks") + action_type = "toggle_periodic_tasks" + success_verb = _("toggled") - menu_icon = "date" - model = CrontabSchedule + @classmethod + def execute_action(cls, objects, **kwargs): + rows_updated = objects.update( + enabled=Case( + When(enabled=True, then=Value(False)), + default=Value(True), + ) + ) + PeriodicTasks.update_changed() + return rows_updated, 0 -class SolarScheduleAdmin(ModelAdmin): - """Admin-interface for clocked schedules.""" - menu_icon = "date" - model = SolarSchedule +@hooks.register("register_bulk_action") +class RunTasksBulkAction(PeriodicTaskBulkAction): + display_name = _("Run") + aria_label = _("Run selected tasks") + action_type = "run_periodic_tasks" + success_verb = _("run") + @classmethod + def execute_action(cls, objects, **kwargs): + action = kwargs["self"] + tasks_run = 0 + for task in objects: + if execute_task(task, action.request.user): + tasks_run += 1 + + return tasks_run, 0 -class TasksModelsAdminGroup(ModelAdminGroup): - menu_label = _("Tasks") - menu_icon = "cogs" - menu_order = get_menu_order("tasks") - items = ( - PeriodicTaskAdmin, - CrontabScheduleAdmin, - IntervalScheduleAdmin, - ClockedScheduleAdmin, - SolarScheduleAdmin, - ) +@hooks.register("register_snippet_listing_buttons") +def register_periodic_task_run_button(snippet, user, next_url=None): + if not isinstance(snippet, PeriodicTask) or not user.has_perm( + "django_celery_beat.change_periodictask" + ): + return -modeladmin_register(TasksModelsAdminGroup) + yield Button( + label=_("Run"), + url=reverse("django_celery_beat:task_run") + f"?task_id={snippet.pk}", + icon_name="play", + priority=10, + ) @hooks.register("register_admin_urls") diff --git a/requirements/base.txt b/requirements/base.txt index f3d0f851..6421f9b2 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -42,10 +42,6 @@ wagtailmenus==4.0.6 # ------------------------------------------------------------------------------ wagtail-localize==1.13 -#Wagtail Admin -# ------------------------------------------------------------------------------ -wagtail-modeladmin==2.2.0 - # Django celery # ------------------------------------------------------------------------------ django-celery-beat==2.9.0 # https://github.com/celery/django-celery-beat From deb1d1b0ac608e2d7c6690380dd5cbfb4834d1e6 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 19:09:55 -0300 Subject: [PATCH 02/47] =?UTF-8?q?Corrige=20a=20contabiliza=C3=A7=C3=A3o=20?= =?UTF-8?q?de=20acessos=20a=20livros=20e=20cap=C3=ADtulos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preserva os identificadores de capítulos dos registros e distribui downloads integrais no escopo de item, mantendo a contagem de título uma vez por sessão. Alinha as chaves canônicas, acrescenta dados de publicação e cobre o processamento com testes. As etapas internas ficam separadas por blocos lógicos. --- metrics/counter/access/accumulation.py | 1 + metrics/counter/access/daily_accumulator.py | 11 ++- metrics/counter/indexing/engines/base.py | 12 +++ metrics/counter/indexing/engines/book.py | 32 ++++--- metrics/opensearch/mappings.py | 2 + metrics/opensearch/painless.py | 23 +++-- metrics/services/metadata_documents.py | 5 ++ .../tests/counter/access/test_accumulation.py | 39 ++++++++- .../tests/counter/indexing/test_converter.py | 83 ++++++++++++++++++- .../integration/test_books_log_to_metrics.py | 52 +++++++++++- 10 files changed, 238 insertions(+), 22 deletions(-) diff --git a/metrics/counter/access/accumulation.py b/metrics/counter/access/accumulation.py index a00ef547..6d3147fc 100644 --- a/metrics/counter/access/accumulation.py +++ b/metrics/counter/access/accumulation.py @@ -74,6 +74,7 @@ def _build_record( "pid_v3": pid_v3, "pid_generic": pid_generic, "title_pid_generic": counter_access.get("title_pid_generic") or pid_generic, + "segment_pid_generics": counter_access.get("segment_pid_generics") or [], "media_format": media_format, "content_language": content_language, "content_type": content_type, diff --git a/metrics/counter/access/daily_accumulator.py b/metrics/counter/access/daily_accumulator.py index b7281630..f71ef76a 100644 --- a/metrics/counter/access/daily_accumulator.py +++ b/metrics/counter/access/daily_accumulator.py @@ -1,6 +1,6 @@ from struct import Struct -_ACCESS_KEY = Struct("!10I") +_ACCESS_KEY = Struct("!11I") _SESSION_KEY = Struct("!5I") @@ -23,6 +23,7 @@ class _CompactAccessRecord: "pid_v3", "publication_year", "session", + "segment_pid_generics", "source_id", "source_type", "source_key", @@ -37,6 +38,10 @@ def __init__(self, accumulator, data, session, url, second): self.pid_v3 = accumulator._intern(data.get("pid_v3")) self.pid_generic = accumulator._intern(data.get("pid_generic")) self.title_pid_generic = accumulator._intern(data.get("title_pid_generic")) + self.segment_pid_generics = tuple( + accumulator._intern(pid_generic) + for pid_generic in data.get("segment_pid_generics") or [] + ) self.media_format = accumulator._intern(data.get("media_format")) self.content_language = accumulator._intern(data.get("content_language")) self.content_type = accumulator._intern(data.get("content_type")) @@ -77,6 +82,9 @@ def as_dict(self, accumulator): "pid_v3": accumulator._resolve(self.pid_v3), "pid_generic": accumulator._resolve(self.pid_generic), "title_pid_generic": accumulator._resolve(self.title_pid_generic), + "segment_pid_generics": [ + accumulator._resolve(pid_id) for pid_id in self.segment_pid_generics + ], "user_session_id": self.session, "click_timestamps_by_url": self._timestamps_as_dict(accumulator), "media_format": accumulator._resolve(self.media_format), @@ -129,6 +137,7 @@ def accumulate_access(self, data, session_key, url, second): self._intern(data.get("pid_v2")), self._intern(data.get("pid_v3")), self._intern(data.get("pid_generic")), + self._intern(tuple(data.get("segment_pid_generics") or ())), session, self._intern(data.get("access_country_code")), self._intern(data.get("content_language")), diff --git a/metrics/counter/indexing/engines/base.py b/metrics/counter/indexing/engines/base.py index 2803979c..502f596f 100644 --- a/metrics/counter/indexing/engines/base.py +++ b/metrics/counter/indexing/engines/base.py @@ -97,6 +97,7 @@ def _build_document(self, value, projection=None, **kwargs): "source_key": source_identifier, "document_key": document_identifier, "month": self._access_month(value), + "publication_year": self._publication_year(value), **counter, "total_requests": 0, "total_investigations": 0, @@ -151,6 +152,17 @@ def _projection_values(value, projection): def _resolve_parent_data_type(self, value): return None + @staticmethod + def _publication_year(value): + try: + year = int(value.get("publication_year")) + except (TypeError, ValueError): + return 1 + + if 1 <= year <= 9999: + return year + return 1 + def _build_counter_section(self, value): return _strip_empty_values( { diff --git a/metrics/counter/indexing/engines/book.py b/metrics/counter/indexing/engines/book.py index d9855698..e7fc803d 100644 --- a/metrics/counter/indexing/engines/book.py +++ b/metrics/counter/indexing/engines/book.py @@ -13,13 +13,27 @@ def partition_key(self, value, projection=None): metric_scope="title", pid_generic=title_pid_generic, ) + return self._generate_document_id(value, projection=projection) def accumulate(self, data, unique_state, value, projection=None): if not isinstance(value, dict): return - if _should_create_item_document(value): + segment_pid_generics = value.get("segment_pid_generics") or [] + if ( + value.get("document_type") == "book" + and is_request(value.get("content_type")) + and segment_pid_generics + ): + for pid_generic in segment_pid_generics: + segment = { + **value, + "pid_generic": pid_generic, + "document_type": "chapter", + } + self._accumulate_item(data, unique_state, segment, projection) + elif value.get("pid_generic"): self._accumulate_item(data, unique_state, value, projection) title_pid_generic = _extract_title_pid_generic(value) @@ -42,12 +56,14 @@ def _accumulate_item(self, data, unique_state, value, projection): metric_scope="item", ), ) + self._apply_totals( document=item_document, click_timestamps=value.get("click_timestamps"), click_timestamps_by_url=value.get("click_timestamps_by_url"), content_type=value.get("content_type"), ) + self._apply_uniques( document=item_document, unique_state=unique_state, @@ -75,12 +91,14 @@ def _accumulate_title( pid_generic=title_pid_generic, ), ) + self._apply_totals( document=title_document, click_timestamps=value.get("click_timestamps"), click_timestamps_by_url=value.get("click_timestamps_by_url"), content_type=value.get("content_type"), ) + self._apply_uniques( document=title_document, unique_state=unique_state, @@ -136,6 +154,7 @@ def _build_document(self, value, projection=None, **kwargs): metric_scope=metric_scope, ), "month": self._access_month(value), + "publication_year": self._publication_year(value), **counter, "total_requests": 0, "total_investigations": 0, @@ -144,17 +163,8 @@ def _build_document(self, value, projection=None, **kwargs): } base_document["daily_metrics"] = self._build_daily_metrics(value) - return _strip_empty_values(base_document) - -def _should_create_item_document(value): - if not value.get("pid_generic"): - return False - if value.get("document_type") == "book" and not is_request( - value.get("content_type") - ): - return False - return True + return _strip_empty_values(base_document) def _extract_title_pid_generic(value): diff --git a/metrics/opensearch/mappings.py b/metrics/opensearch/mappings.py index 23b84f64..ad10e7a9 100644 --- a/metrics/opensearch/mappings.py +++ b/metrics/opensearch/mappings.py @@ -20,6 +20,7 @@ "source_key": {"type": "keyword"}, "document_key": {"type": "keyword"}, "month": {"type": "date", "format": "yyyy-MM"}, + "publication_year": {"type": "integer"}, "applied_days": { "type": "keyword", "index": False, @@ -98,6 +99,7 @@ "parent_document_key": {"type": "keyword"}, "title": {"type": "text", "fields": {"keyword": {"type": "keyword"}}}, "publication_year": {"type": "integer"}, + "publication_date": {"type": "keyword"}, "default_lang": {"type": "keyword"}, "text_langs": {"type": "keyword"}, "identifiers": {"type": "flat_object"}, diff --git a/metrics/opensearch/painless.py b/metrics/opensearch/painless.py index 737672a0..203d6e2d 100644 --- a/metrics/opensearch/painless.py +++ b/metrics/opensearch/painless.py @@ -22,6 +22,13 @@ if (!params.metric_fields.contains(entry.getKey()) && !'applied_days'.equals(entry.getKey()) && !'daily_metrics'.equals(entry.getKey())) { + if ('publication_year'.equals(entry.getKey()) + && ctx._source.containsKey(entry.getKey()) + && ctx._source[entry.getKey()] != null + && ctx._source[entry.getKey()] != 1 + && entry.getValue() == 1) { + continue; + } if (!ctx._source.containsKey(entry.getKey()) || ctx._source[entry.getKey()] != entry.getValue()) { ctx._source[entry.getKey()] = entry.getValue(); } @@ -144,13 +151,15 @@ def merge_metric_document(existing, current, operation="add"): return current merged = dict(existing) - merged.update( - { - key: value - for key, value in current.items() - if key not in METRIC_FIELDS and key != "daily_metrics" - } - ) + metadata = { + key: value + for key, value in current.items() + if key not in METRIC_FIELDS and key != "daily_metrics" + } + if existing.get("publication_year") not in {None, 1}: + if metadata.get("publication_year") == 1: + metadata.pop("publication_year") + merged.update(metadata) signal = -1 if operation == "subtract" else 1 for field in METRIC_FIELDS: diff --git a/metrics/services/metadata_documents.py b/metrics/services/metadata_documents.py index 7bb521f9..4a77ac44 100644 --- a/metrics/services/metadata_documents.py +++ b/metrics/services/metadata_documents.py @@ -34,6 +34,8 @@ def build_document_document(document, active=True): or document.pid_generic or document.document_id ) + if document.document_type in {"book", "chapter"} and document.pid_generic: + canonical_id = document.pid_generic.upper() key = document_key( collection, document.document_type, @@ -52,6 +54,8 @@ def build_document_document(document, active=True): parent_id = ( parent.pid_v3 or parent.pid_v2 or parent.pid_generic or parent.document_id ) + if parent.document_type in {"book", "chapter"} and parent.pid_generic: + parent_id = parent.pid_generic.upper() parent_identifier = document_key( collection, parent.document_type, @@ -66,6 +70,7 @@ def build_document_document(document, active=True): "parent_document_key": parent_identifier, "title": document.title, "publication_year": _integer_or_none(document.publication_year), + "publication_date": document.publication_date, "default_lang": document.default_lang, "text_langs": document.text_langs or [], "identifiers": _document_identifiers(document), diff --git a/metrics/tests/counter/access/test_accumulation.py b/metrics/tests/counter/access/test_accumulation.py index 6c81f0be..666a75db 100644 --- a/metrics/tests/counter/access/test_accumulation.py +++ b/metrics/tests/counter/access/test_accumulation.py @@ -89,7 +89,7 @@ def test_rejects_invalid_local_datetime(self): ) self.assertEqual(len(results), 0) - def test_does_not_expand_book_into_segments(self): + def test_preserves_book_segments_for_conversion(self): results = DailyAccessAccumulator() counter_access = self._book_counter_access( source_id="c2248", @@ -109,6 +109,43 @@ def test_does_not_expand_book_into_segments(self): self.assertEqual(len(results), 1) result = list(results.iter_materialized_values())[0] self.assertEqual(result["pid_generic"], "BOOK:C2248") + self.assertEqual( + result["segment_pid_generics"], + [ + "BOOK:C2248/CHAPTER:00", + "BOOK:C2248/CHAPTER:01", + "BOOK:C2248/CHAPTER:02", + ], + ) + + def test_separates_accesses_when_book_segments_change(self): + results = DailyAccessAccumulator() + book = self._book_counter_access( + segment_pid_generics=["BOOK:Q7GTD/CHAPTER:01"], + ) + + accumulation.accumulate(results, book, self._line()) + accumulation.accumulate( + results, + { + **book, + "segment_pid_generics": [ + "BOOK:Q7GTD/CHAPTER:01", + "BOOK:Q7GTD/CHAPTER:02", + ], + }, + self._line(local_datetime=datetime(2024, 1, 15, 10, 1, 5)), + ) + + result = list(results.iter_materialized_values()) + self.assertEqual(len(result), 2) + self.assertEqual( + [item["segment_pid_generics"] for item in result], + [ + ["BOOK:Q7GTD/CHAPTER:01"], + ["BOOK:Q7GTD/CHAPTER:01", "BOOK:Q7GTD/CHAPTER:02"], + ], + ) def test_double_click_filter_uses_url_bucket_for_same_item(self): results = DailyAccessAccumulator() diff --git a/metrics/tests/counter/indexing/test_converter.py b/metrics/tests/counter/indexing/test_converter.py index 30e99995..e8b1f318 100644 --- a/metrics/tests/counter/indexing/test_converter.py +++ b/metrics/tests/counter/indexing/test_converter.py @@ -1,4 +1,4 @@ -from scielo_usage_counter.values import CONTENT_TYPE_FULL_TEXT +from scielo_usage_counter.values import CONTENT_TYPE_ABSTRACT, CONTENT_TYPE_FULL_TEXT from metrics.counter.indexing import converter @@ -32,6 +32,7 @@ def test_counter_facts_are_monthly_and_reference_metadata_keys(): document_id, document = next(iter(documents.items())) assert document_id.startswith("k1_") assert document["month"] == "2026-08" + assert document["publication_year"] == 2025 assert document["data_type"] == "Article" assert document["parent_data_type"] == "Journal" assert document["total_requests"] == 1 @@ -100,6 +101,86 @@ def test_book_and_chapter_keep_item_and_title_scopes(): "Book_Segment", } assert all(item["total_requests"] == 1 for item in documents.values()) + assert all(item["publication_year"] == 2025 for item in documents.values()) + + +def test_book_investigation_counts_item_and_title_scopes(): + book = { + **_article(), + "collection": "books", + "source_key": "book-1", + "document_type": "book", + "pid_v3": None, + "pid_generic": "BOOK:BOOK-1", + "title_pid_generic": "BOOK:BOOK-1", + "content_type": CONTENT_TYPE_ABSTRACT, + "source": {"source_type": "book", "source_id": "book-1"}, + } + + documents = dict(converter.iter_partitioned_values([book], "counter")) + + assert len(documents) == 2 + assert {item["metric_scope"] for item in documents.values()} == {"item", "title"} + assert all(item["total_investigations"] == 1 for item in documents.values()) + assert all(item["unique_investigations"] == 1 for item in documents.values()) + + +def test_whole_book_request_counts_each_identified_chapter_once(): + book = { + **_article(), + "collection": "books", + "source_key": "book-1", + "document_type": "book", + "pid_v3": None, + "pid_generic": "BOOK:BOOK-1", + "title_pid_generic": "BOOK:BOOK-1", + "segment_pid_generics": [ + "BOOK:BOOK-1/CHAPTER:01", + "BOOK:BOOK-1/CHAPTER:02", + "BOOK:BOOK-1/CHAPTER:03", + ], + "source": {"source_type": "book", "source_id": "book-1"}, + } + chapter = { + **book, + "document_type": "chapter", + "pid_generic": "BOOK:BOOK-1/CHAPTER:01", + "segment_pid_generics": [], + } + + for dataset in ("counter", "analytics"): + documents = dict(converter.iter_partitioned_values([book, chapter], dataset)) + items = [item for item in documents.values() if item["metric_scope"] == "item"] + titles = [ + item for item in documents.values() if item["metric_scope"] == "title" + ] + + assert len(items) == 3 + assert len(titles) == 1 + assert sum(item["total_requests"] for item in items) == 4 + assert sum(item["unique_requests"] for item in items) == 3 + assert titles[0]["total_requests"] == 2 + assert titles[0]["unique_requests"] == 1 + + +def test_whole_book_without_identifiable_chapters_counts_one_item(): + book = { + **_article(), + "collection": "books", + "source_key": "book-1", + "document_type": "book", + "pid_v3": None, + "pid_generic": "BOOK:BOOK-1", + "title_pid_generic": "BOOK:BOOK-1", + "segment_pid_generics": [], + "source": {"source_type": "book", "source_id": "book-1"}, + } + + documents = dict(converter.iter_partitioned_values([book], "counter")) + + assert len(documents) == 2 + assert {item["metric_scope"] for item in documents.values()} == {"item", "title"} + assert all(item["total_requests"] == 1 for item in documents.values()) def test_compact_keys_and_output_are_deterministic(): diff --git a/metrics/tests/integration/test_books_log_to_metrics.py b/metrics/tests/integration/test_books_log_to_metrics.py index b156eaca..f5968026 100644 --- a/metrics/tests/integration/test_books_log_to_metrics.py +++ b/metrics/tests/integration/test_books_log_to_metrics.py @@ -2,13 +2,13 @@ from datetime import datetime from pathlib import Path +from scielo_usage_counter import log_handler from scielo_usage_counter.translator.books import URLTranslatorBooksSite from scielo_usage_counter.url_translator import URLTranslationManager from metrics.counter.access import accumulation, extraction, validation from metrics.counter.access.daily_accumulator import DailyAccessAccumulator from metrics.tests.helpers import convert_accumulator -from scielo_usage_counter import log_handler FIXTURES_DIR = Path(__file__).resolve().parent.parent / "fixtures" @@ -136,6 +136,56 @@ def test_full_pipeline_with_synthetic_metadata(self): self.assertTrue(has_item) self.assertTrue(has_title) + def test_whole_book_pdf_counts_chapters_from_metadata(self): + book_id = "xjcw9" + manager = URLTranslationManager( + documents_metadata=iter( + [ + { + "document_id": f"book:{book_id}/chapter:{chapter_id}", + "document_type": "chapter", + "pid_generic": f"book:{book_id}/chapter:{chapter_id}", + "source_id": book_id, + "source_type": "book", + "title": f"Chapter {chapter_id}", + } + for chapter_id in ("01", "02") + ] + ), + sources_metadata=iter( + [{"source_id": book_id, "source_type": "book", "title": "Test Book"}] + ), + translator=URLTranslatorBooksSite, + ) + translated = manager.translate(f"/id/{book_id}/pdf/test-book.pdf") + counter_access = extraction.extract("books", translated) + + self.assertEqual(len(counter_access["segment_pid_generics"]), 2) + + results = DailyAccessAccumulator() + accumulation.accumulate( + results, + counter_access, + { + "client_name": "browser", + "client_version": "1.0", + "ip_address": "186.215.90.179", + "country_code": "BR", + "local_datetime": datetime(2025, 8, 1, 10, 0, 29), + }, + ) + + metrics = convert_accumulator(results) + for dataset in ("counter", "analytics"): + documents = metrics[dataset].values() + items = [item for item in documents if item["metric_scope"] == "item"] + titles = [item for item in documents if item["metric_scope"] == "title"] + + self.assertEqual(len(items), 2) + self.assertEqual(len(titles), 1) + self.assertEqual(sum(item["unique_requests"] for item in items), 2) + self.assertEqual(titles[0]["unique_requests"], 1) + def test_all_metric_fields_present_in_converted_document(self): results = DailyAccessAccumulator() counter_access = extraction.extract( From f9198d6c6c09e8c1940994692f209b496ab7f5fd Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 19:10:00 -0300 Subject: [PATCH 03/47] Permite reconstruir integralmente os metadados de uso MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adiciona a sincronização completa com reinício do cursor de fontes ou documentos, propaga a opção pela tarefa assíncrona e verifica a compatibilidade das chaves de capítulos. --- metrics/services/metadata_sync.py | 22 ++++++-- metrics/tasks/metadata_sync.py | 9 +++- metrics/tests/services/test_metadata_sync.py | 55 +++++++++++++++++++- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/metrics/services/metadata_sync.py b/metrics/services/metadata_sync.py index bc20e9cd..541360d0 100644 --- a/metrics/services/metadata_sync.py +++ b/metrics/services/metadata_sync.py @@ -21,7 +21,7 @@ LEASE_MINUTES = 30 -def sync_metadata(entity=None, batch_size=DEFAULT_BATCH_SIZE): +def sync_metadata(entity=None, batch_size=DEFAULT_BATCH_SIZE, full=False): entities = ( [entity] if entity @@ -36,13 +36,24 @@ def sync_metadata(entity=None, batch_size=DEFAULT_BATCH_SIZE): raise RuntimeError("OpenSearch client is not available.") for current_entity in entities: - result[current_entity] = _sync_entity(client, current_entity, batch_size) + result[current_entity] = _sync_entity( + client, + current_entity, + batch_size, + full, + ) + return result -def _sync_entity(client, entity, batch_size): +def _sync_entity(client, entity, batch_size, full): model, serializer, related = _entity_config(entity) state = _acquire_lease(entity) + + if full: + state.cursor_updated = None + state.cursor_pk = 0 + alias = generate_metadata_alias( settings.OPENSEARCH_INDEX_NAME, f"{entity}s", @@ -55,6 +66,7 @@ def _sync_entity(client, entity, batch_size): batch = list(_cursor_queryset(model, state, related)[:batch_size]) if not batch: break + exported += client.index_document_items( alias, (serializer(item) for item in batch), @@ -66,6 +78,7 @@ def _sync_entity(client, entity, batch_size): exported += _flush_outbox(client, entity, alias, batch_size) state.completed_at = timezone.now() + return exported finally: state.lease_until = None @@ -102,6 +115,7 @@ def _cursor_queryset(model, state, related): queryset = model.objects.select_related(*related).order_by("updated", "pk") if state.cursor_updated is None: return queryset + return queryset.filter( Q(updated__gt=state.cursor_updated) | Q(updated=state.cursor_updated, pk__gt=state.cursor_pk) @@ -116,9 +130,11 @@ def _acquire_lease(entity): ) if state.lease_until and state.lease_until > now: raise RuntimeError(f"Metadata sync already running for {entity}.") + state.lease_until = now + timedelta(minutes=LEASE_MINUTES) state.heartbeat_at = now state.save(update_fields=["lease_until", "heartbeat_at"]) + return state diff --git a/metrics/tasks/metadata_sync.py b/metrics/tasks/metadata_sync.py index dfc92a80..0f1befa4 100644 --- a/metrics/tasks/metadata_sync.py +++ b/metrics/tasks/metadata_sync.py @@ -8,5 +8,10 @@ queue="load", timelimit=-1, ) -def task_sync_opensearch_metadata(self, entity=None, batch_size=DEFAULT_BATCH_SIZE): - return sync_metadata(entity=entity, batch_size=batch_size) +def task_sync_opensearch_metadata( + self, + entity=None, + batch_size=DEFAULT_BATCH_SIZE, + full=False, +): + return sync_metadata(entity=entity, batch_size=batch_size, full=full) diff --git a/metrics/tests/services/test_metadata_sync.py b/metrics/tests/services/test_metadata_sync.py index 660d3966..21b105a9 100644 --- a/metrics/tests/services/test_metadata_sync.py +++ b/metrics/tests/services/test_metadata_sync.py @@ -1,9 +1,11 @@ from unittest.mock import Mock, patch from django.test import TestCase, override_settings +from scielo_usage_counter.values import CONTENT_TYPE_FULL_TEXT from collection.models import Collection from document.models import Document +from metrics.counter.indexing import converter from metrics.models import MetadataSyncOutbox, MetadataSyncState from metrics.services.metadata_documents import ( build_document_document, @@ -46,6 +48,50 @@ def test_metadata_is_stored_once_under_the_same_keys_used_by_facts(self): self.assertTrue(source_payload["active"]) self.assertTrue(document_payload["active"]) + def test_book_metadata_uses_the_same_canonical_key_as_facts(self): + collection = Collection.objects.create(acron3="books", acron2="bk") + source = Source.objects.create( + collection=collection, + source_type=Source.SOURCE_TYPE_BOOK, + source_id="b5b72", + title="Book title", + ) + document = Document.objects.create( + collection=collection, + source=source, + document_type=Document.DOCUMENT_TYPE_CHAPTER, + document_id="book:b5b72/chapter:8jp9n", + pid_generic="book:b5b72/chapter:8jp9n", + title="Chapter title", + ) + metadata_key, _payload = build_document_document(document) + facts = dict( + converter.iter_partitioned_values( + [ + { + "collection": "books", + "document_type": "chapter", + "pid_generic": "BOOK:B5B72/CHAPTER:8JP9N", + "title_pid_generic": "BOOK:B5B72", + "source": { + "source_type": "book", + "source_id": "b5b72", + }, + "content_type": CONTENT_TYPE_FULL_TEXT, + "click_timestamps": {"00:01": 1}, + "user_session_id": "session-1", + "access_date": "2025-02-01", + } + ], + "counter", + ) + ) + item = next( + value for value in facts.values() if value["metric_scope"] == "item" + ) + + self.assertEqual(item["document_key"], metadata_key) + def test_save_and_delete_create_transactional_outbox_entries(self): source_key, _payload = build_source_document(self.source) document_key, _payload = build_document_document(self.document) @@ -66,13 +112,18 @@ def test_save_and_delete_create_transactional_outbox_entries(self): @override_settings(OPENSEARCH_INDEX_NAME="custom_usage") @patch("metrics.services.metadata_sync.OpenSearchUsageClient") - def test_sync_advances_cursor_and_clears_exported_outbox(self, client_class): + def test_full_sync_resets_cursor_and_clears_exported_outbox(self, client_class): client = Mock() client.ping.return_value = True client.index_document_items.side_effect = lambda index, items: len(list(items)) client_class.return_value = client + MetadataSyncState.objects.create( + entity=MetadataSyncState.ENTITY_SOURCE, + cursor_updated=self.source.updated, + cursor_pk=self.source.pk + 1, + ) - exported = sync_metadata(entity=MetadataSyncState.ENTITY_SOURCE) + exported = sync_metadata(entity=MetadataSyncState.ENTITY_SOURCE, full=True) state = MetadataSyncState.objects.get(entity=MetadataSyncState.ENTITY_SOURCE) self.assertGreaterEqual(exported[MetadataSyncState.ENTITY_SOURCE], 1) From 6b692e0c4c7c45437e59f3e34526f99ae9db78e2 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 19:10:05 -0300 Subject: [PATCH 04/47] =?UTF-8?q?Ajusta=20a=20cria=C3=A7=C3=A3o=20do=20cli?= =?UTF-8?q?ente=20do=20OpenSearch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interpreta credenciais básicas no formato usuário e senha, preserva autenticação por chave e organiza as opções comuns de conexão. Acrescenta testes para credenciais e preservação do ano de publicação. --- metrics/opensearch/client.py | 38 +++++++++++++------------ metrics/tests/opensearch/test_client.py | 22 ++++++++++++++ 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/metrics/opensearch/client.py b/metrics/opensearch/client.py index 675b678e..6b060145 100644 --- a/metrics/opensearch/client.py +++ b/metrics/opensearch/client.py @@ -51,27 +51,28 @@ def get_opensearch_client( api_key = api_key or getattr(settings, "OPENSEARCH_API_KEY", None) if verify_certs is None: verify_certs = getattr(settings, "OPENSEARCH_VERIFY_CERTS", False) + http_compress = getattr(settings, "OPENSEARCH_HTTP_COMPRESS", True) + options = { + "verify_certs": verify_certs, + "http_compress": http_compress, + } if basic_auth: - return OpenSearch( - url, - http_auth=tuple(basic_auth), - verify_certs=verify_certs, - http_compress=http_compress, - ) - if api_key: - return OpenSearch( - url, - api_key=api_key, - verify_certs=verify_certs, - http_compress=http_compress, - ) - return OpenSearch( - url, - verify_certs=verify_certs, - http_compress=http_compress, - ) + if isinstance(basic_auth, str): + username, separator, password = basic_auth.partition(":") + if not separator: + raise ValueError( + "OPENSEARCH_BASIC_AUTH must use the username:password format." + ) + + basic_auth = (username, password) + + options["http_auth"] = tuple(basic_auth) + elif api_key: + options["api_key"] = api_key + + return OpenSearch(url, **options) def ping(self): try: @@ -126,6 +127,7 @@ def create_alias_if_not_exists( ): if ping_client and not self.ping(): return + if self.client.indices.exists_alias(name=alias_name): return diff --git a/metrics/tests/opensearch/test_client.py b/metrics/tests/opensearch/test_client.py index 7a9a3740..565ca516 100644 --- a/metrics/tests/opensearch/test_client.py +++ b/metrics/tests/opensearch/test_client.py @@ -11,9 +11,31 @@ SOURCE_INDEX_MAPPINGS, get_index_settings, ) +from metrics.opensearch.painless import merge_metric_document class OpenSearchUsageClientTests(SimpleTestCase): + @patch("metrics.opensearch.client.OpenSearch") + def test_basic_auth_string_is_split_into_credentials(self, opensearch): + OpenSearchUsageClient( + url="https://example.org:9200", + basic_auth="reader:secret", + ) + + self.assertEqual( + opensearch.call_args.kwargs["http_auth"], + ("reader", "secret"), + ) + + def test_unknown_yop_does_not_replace_known_yop(self): + existing = {"publication_year": 2024, "total_requests": 1} + current = {"publication_year": 1, "total_requests": 2} + + merged = merge_metric_document(existing, current) + + assert merged["publication_year"] == 2024 + assert merged["total_requests"] == 3 + def test_rejects_non_positive_primary_shard_count(self): with self.assertRaisesMessage( ValueError, From 48eb750ed19bb89e637b065242c563cfe7fa3848 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:27:53 -0300 Subject: [PATCH 05/47] =?UTF-8?q?Adiciona=20depend=C3=AAncias=20da=20inter?= =?UTF-8?q?face=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inclui geração de esquema, planilhas, normalização de identificadores e validação local do esquema oficial. --- requirements/base.txt | 3 +++ requirements/local.txt | 1 + 2 files changed, 4 insertions(+) diff --git a/requirements/base.txt b/requirements/base.txt index 6421f9b2..48a6caca 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -24,6 +24,7 @@ django-redis==5.4.0 # https://github.com/jazzband/django-redis4 # Django REST djangorestframework==3.15.2 djangorestframework-simplejwt==5.3.1 # https://django-rest-framework-simplejwt.readthedocs.io/en/latest/ +drf-spectacular==0.30.0 # Wagtail # ------------------------------------------------------------------------------ @@ -98,3 +99,5 @@ articlemetaapi==1.26.7 # OpenSearch # ------------------------------------------------------------------------------ opensearch-py==3.1.0 +openpyxl==3.1.5 +python-stdnum==2.2 diff --git a/requirements/local.txt b/requirements/local.txt index e8ab2901..a746553e 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -11,6 +11,7 @@ mypy==1.10.0 # https://github.com/python/mypy django-stubs==5.2.9 # https://github.com/typeddjango/django-stubs pytest==7.4.3 # https://github.com/pytest-dev/pytest pytest-sugar==0.9.7 # https://github.com/Frozenball/pytest-sugar +jsonschema==4.26.0 # Documentation # ------------------------------------------------------------------------------ From 4c64a0fa371b808e6e05ac6117e24f4386e1a3dd Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:28:45 -0300 Subject: [PATCH 06/47] =?UTF-8?q?Prepara=20a=20aplica=C3=A7=C3=A3o=20COUNT?= =?UTF-8?q?ER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registra a aplicação, suas configurações de relatórios e a documentação automática no projeto Django. --- config/settings/base.py | 46 +++++++++++++++++++++++++++++++++-- counter_api/__init__.py | 0 counter_api/apps.py | 6 +++++ counter_api/tests/__init__.py | 0 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 counter_api/__init__.py create mode 100644 counter_api/apps.py create mode 100644 counter_api/tests/__init__.py diff --git a/config/settings/base.py b/config/settings/base.py index bc17f12f..50d70bfc 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -1,6 +1,7 @@ """ Base settings to build other settings files upon. """ + from datetime import timedelta from pathlib import Path @@ -133,6 +134,7 @@ "wagtailcaptcha", "wagtailmenus", "rest_framework", + "drf_spectacular", ] LOCAL_APPS = [ @@ -145,6 +147,7 @@ "log_manager", "log_manager_config", "metrics", + "counter_api", "reports", "resources", "source", @@ -429,11 +432,38 @@ REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": env.int("DRF_PAGE_SIZE", default=10), + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", ), } +SPECTACULAR_SETTINGS = { + "TITLE": "SciELO COUNTER API", + "DESCRIPTION": ( + "COUNTER Project Release 5.1 API with SciELO extensions. " + "Report availability follows completed months in OpenSearch." + ), + "VERSION": "5.1", + "SERVE_INCLUDE_SCHEMA": False, + "SERVE_AUTHENTICATION": [], + "SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"], + "SWAGGER_UI_SETTINGS": { + "deepLinking": True, + "displayOperationId": True, + "persistAuthorization": False, + }, + "APPEND_COMPONENTS": { + "securitySchemes": { + "CounterAPIKey": { + "type": "apiKey", + "in": "query", + "name": "api_key", + } + } + }, +} + # JWT # ------------------------------------------------------------------------------ SIMPLE_JWT = { @@ -475,10 +505,22 @@ COUNTER_ROBOTS_URL = env( "COUNTER_ROBOTS_URL", default=( - "https://raw.githubusercontent.com/atmire/COUNTER-Robots/master/" - "COUNTER_Robots_list.json" + "https://raw.githubusercontent.com/Project-Counter/counter-bots/" + "counter-bots/counter_bots.json" ), ) +COUNTER_REGISTRY_URL = env("COUNTER_REGISTRY_URL", default="") +COUNTER_CUSTOMER_ID = env("COUNTER_CUSTOMER_ID", default="0000000000000000") +COUNTER_INSTITUTION_NAME = env("COUNTER_INSTITUTION_NAME", default="The World") +COUNTER_CREATED_BY = env("COUNTER_CREATED_BY", default="SciELO") +COUNTER_QUERY_BUDGET_SECONDS = env.int( + "COUNTER_QUERY_BUDGET_SECONDS", + default=25, +) +COUNTER_MAX_REPORT_ENTRIES = env.int( + "COUNTER_MAX_REPORT_ENTRIES", + default=100000, +) MMDB_URL_TEMPLATE = env( "MMDB_URL_TEMPLATE", default=( diff --git a/counter_api/__init__.py b/counter_api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/counter_api/apps.py b/counter_api/apps.py new file mode 100644 index 00000000..4ec0c05c --- /dev/null +++ b/counter_api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CounterAPIConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "counter_api" diff --git a/counter_api/tests/__init__.py b/counter_api/tests/__init__.py new file mode 100644 index 00000000..e69de29b From b0407affc31fe30b8cc634fd6f70bfc74221839a Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:28:45 -0300 Subject: [PATCH 07/47] =?UTF-8?q?Adiciona=20chaves=20de=20acesso=20=C3=A0?= =?UTF-8?q?=20interface=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Armazena apenas o resumo criptográfico das chaves e oferece administração e comando para emissão controlada. --- counter_api/admin.py | 21 +++++++++ counter_api/management/__init__.py | 0 counter_api/management/commands/__init__.py | 0 .../commands/issue_counter_api_key.py | 26 +++++++++++ counter_api/migrations/0001_initial.py | 36 +++++++++++++++ counter_api/migrations/__init__.py | 0 counter_api/models.py | 45 +++++++++++++++++++ 7 files changed, 128 insertions(+) create mode 100644 counter_api/admin.py create mode 100644 counter_api/management/__init__.py create mode 100644 counter_api/management/commands/__init__.py create mode 100644 counter_api/management/commands/issue_counter_api_key.py create mode 100644 counter_api/migrations/0001_initial.py create mode 100644 counter_api/migrations/__init__.py create mode 100644 counter_api/models.py diff --git a/counter_api/admin.py b/counter_api/admin.py new file mode 100644 index 00000000..578f92c3 --- /dev/null +++ b/counter_api/admin.py @@ -0,0 +1,21 @@ +from django.contrib import admin + +from counter_api.models import APIKey + + +@admin.register(APIKey) +class APIKeyAdmin(admin.ModelAdmin): + list_display = ( + "name", + "active", + "expires_at", + "last_used_at", + "created_at", + ) + list_filter = ("active",) + search_fields = ("name",) + fields = ("name", "active", "expires_at", "last_used_at", "created_at") + readonly_fields = ("name", "last_used_at", "created_at") + + def has_add_permission(self, request): + return False diff --git a/counter_api/management/__init__.py b/counter_api/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/counter_api/management/commands/__init__.py b/counter_api/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/counter_api/management/commands/issue_counter_api_key.py b/counter_api/management/commands/issue_counter_api_key.py new file mode 100644 index 00000000..5662e44a --- /dev/null +++ b/counter_api/management/commands/issue_counter_api_key.py @@ -0,0 +1,26 @@ +from django.core.management.base import BaseCommand +from django.utils import timezone +from django.utils.dateparse import parse_datetime + +from counter_api.models import APIKey + + +class Command(BaseCommand): + help = "Issue a COUNTER API key and print its value once." + + def add_arguments(self, parser): + parser.add_argument("name") + parser.add_argument("--expires-at") + + def handle(self, *args, **options): + expires_at = None + if options["expires_at"]: + expires_at = parse_datetime(options["expires_at"]) + if expires_at is None: + raise ValueError("expires-at must be an ISO 8601 datetime") + if timezone.is_naive(expires_at): + expires_at = timezone.make_aware(expires_at) + + key, value = APIKey.issue(options["name"], expires_at=expires_at) + self.stdout.write(f"id={key.pk}") + self.stdout.write(f"api_key={value}") diff --git a/counter_api/migrations/0001_initial.py b/counter_api/migrations/0001_initial.py new file mode 100644 index 00000000..60ad17e1 --- /dev/null +++ b/counter_api/migrations/0001_initial.py @@ -0,0 +1,36 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="APIKey", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=120)), + ( + "digest", + models.CharField(editable=False, max_length=64, unique=True), + ), + ("active", models.BooleanField(default=True)), + ("expires_at", models.DateTimeField(blank=True, null=True)), + ( + "last_used_at", + models.DateTimeField(blank=True, editable=False, null=True), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/counter_api/migrations/__init__.py b/counter_api/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/counter_api/models.py b/counter_api/models.py new file mode 100644 index 00000000..2ce7f699 --- /dev/null +++ b/counter_api/models.py @@ -0,0 +1,45 @@ +import hashlib +import secrets + +from django.db import models +from django.utils import timezone + + +class APIKey(models.Model): + name = models.CharField(max_length=120) + digest = models.CharField(max_length=64, unique=True, editable=False) + active = models.BooleanField(default=True) + expires_at = models.DateTimeField(null=True, blank=True) + last_used_at = models.DateTimeField(null=True, blank=True, editable=False) + created_at = models.DateTimeField(auto_now_add=True) + + @classmethod + def issue(cls, name, expires_at=None): + value = secrets.token_urlsafe(32) + digest = hashlib.sha256(value.encode()).hexdigest() + key = cls.objects.create( + name=name, + digest=digest, + expires_at=expires_at, + ) + + return key, value + + @classmethod + def authenticate(cls, value): + if not value: + return None + + digest = hashlib.sha256(value.encode()).hexdigest() + key = cls.objects.filter(digest=digest, active=True).first() + if not key: + return None + + if key.expires_at and key.expires_at <= timezone.now(): + return None + + cls.objects.filter(pk=key.pk).update(last_used_at=timezone.now()) + return key + + def __str__(self): + return self.name From f07916df993803965d81385cb2e31081595ce9d4 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:28:45 -0300 Subject: [PATCH 08/47] =?UTF-8?q?Valida=20per=C3=ADodos=20e=20exce=C3=A7?= =?UTF-8?q?=C3=B5es=20dos=20relat=C3=B3rios=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expande meses completos, rejeita períodos inválidos e representa as exceções padronizadas da especificação. --- counter_api/dates.py | 51 +++++++++++++++++++++++++++++++++ counter_api/exceptions.py | 42 +++++++++++++++++++++++++++ counter_api/tests/test_dates.py | 22 ++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 counter_api/dates.py create mode 100644 counter_api/exceptions.py create mode 100644 counter_api/tests/test_dates.py diff --git a/counter_api/dates.py b/counter_api/dates.py new file mode 100644 index 00000000..062b58d1 --- /dev/null +++ b/counter_api/dates.py @@ -0,0 +1,51 @@ +import calendar +from datetime import date + +from counter_api.exceptions import insufficient_information, invalid_dates + + +def parse_period(params): + begin_value = params.get("begin_date") + end_value = params.get("end_date") + if not begin_value or not end_value: + raise insufficient_information("begin_date and end_date are required") + + begin = _parse_date(begin_value, first=True) + end = _parse_date(end_value, first=False) + if begin > end: + raise invalid_dates("end_date must not be earlier than begin_date") + if begin.day != 1: + raise invalid_dates("begin_date must be the first day of a month") + if end.day != calendar.monthrange(end.year, end.month)[1]: + raise invalid_dates("end_date must be the last day of a month") + if end >= date.today().replace(day=1): + raise invalid_dates("end_date must be earlier than the current month") + + return begin, end + + +def iter_months(begin, end): + current = begin.replace(day=1) + final = end.replace(day=1) + + while current <= final: + yield current + if current.month == 12: + current = current.replace(year=current.year + 1, month=1) + else: + current = current.replace(month=current.month + 1) + + +def _parse_date(value, first): + if len(value) == 7: + try: + year, month = (int(part) for part in value.split("-")) + day = 1 if first else calendar.monthrange(year, month)[1] + return date(year, month, day) + except (TypeError, ValueError): + raise invalid_dates("dates must use YYYY-MM or YYYY-MM-DD") + + try: + return date.fromisoformat(value) + except (TypeError, ValueError): + raise invalid_dates("dates must use YYYY-MM or YYYY-MM-DD") diff --git a/counter_api/exceptions.py b/counter_api/exceptions.py new file mode 100644 index 00000000..4b383f3c --- /dev/null +++ b/counter_api/exceptions.py @@ -0,0 +1,42 @@ +class CounterAPIError(Exception): + def __init__(self, code, message, status_code, data=None): + self.code = code + self.message = message + self.status_code = status_code + self.data = data + + super().__init__(message) + + def as_dict(self): + payload = { + "Code": self.code, + "Message": self.message, + } + if self.data: + payload["Data"] = self.data + return payload + + +def invalid_dates(data=None): + return CounterAPIError(3020, "Invalid Date Arguments", 400, data) + + +def insufficient_information(data=None): + return CounterAPIError( + 1030, + "Insufficient Information to Process Request", + 400, + data, + ) + + +def service_unavailable(data=None): + return CounterAPIError(1000, "Service Not Available", 503, data) + + +def invalid_filter(data=None): + return CounterAPIError(3060, "Invalid ReportFilter Value", 400, data) + + +def incongruous_filter(data=None): + return CounterAPIError(3061, "Incongruous ReportFilter Value", 400, data) diff --git a/counter_api/tests/test_dates.py b/counter_api/tests/test_dates.py new file mode 100644 index 00000000..46510a7f --- /dev/null +++ b/counter_api/tests/test_dates.py @@ -0,0 +1,22 @@ +from datetime import date + +from django.test import SimpleTestCase + +from counter_api.dates import parse_period +from counter_api.exceptions import CounterAPIError + + +class ReportPeriodTests(SimpleTestCase): + def test_expands_months_to_complete_period(self): + begin, end = parse_period({"begin_date": "2024-02", "end_date": "2024-03"}) + + self.assertEqual(begin, date(2024, 2, 1)) + self.assertEqual(end, date(2024, 3, 31)) + + def test_rejects_current_month(self): + current_month = date.today().strftime("%Y-%m") + + with self.assertRaises(CounterAPIError) as raised: + parse_period({"begin_date": "2024-01", "end_date": current_month}) + + self.assertEqual(raised.exception.code, 3020) From 3660fa445cbf31c067c45ca0faad6a8be80bf164 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:28:46 -0300 Subject: [PATCH 09/47] =?UTF-8?q?Define=20os=20contratos=20dos=20relat?= =?UTF-8?q?=C3=B3rios=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centraliza relatórios, métricas, visões padronizadas, tipos de plataforma e limites operacionais. --- counter_api/constants.py | 63 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 counter_api/constants.py diff --git a/counter_api/constants.py b/counter_api/constants.py new file mode 100644 index 00000000..4d1a7bbf --- /dev/null +++ b/counter_api/constants.py @@ -0,0 +1,63 @@ +RELEASE = "5.1" +PROPRIETARY_NAMESPACE = "SciELO" +COMPOSITE_PAGE_SIZE = 1000 +METADATA_BATCH_SIZE = 1000 +EXCEL_MAX_ROWS = 1048576 +SPOOL_SIZE = 8 * 1024 * 1024 +METRICS = { + "Total_Item_Investigations": "total_investigations", + "Unique_Item_Investigations": "unique_investigations", + "Total_Item_Requests": "total_requests", + "Unique_Item_Requests": "unique_requests", +} +TITLE_METRICS = { + "Unique_Title_Investigations": "unique_investigations", + "Unique_Title_Requests": "unique_requests", +} +STANDARD_VIEWS = { + "tr_j3": { + "filters": { + "Data_Type": ["Journal"], + "Access_Method": ["Regular"], + "Metric_Type": list(METRICS), + }, + "attributes": ["Access_Type"], + }, + "tr_b3": { + "filters": { + "Data_Type": ["Book", "Reference_Work"], + "Access_Method": ["Regular"], + "Metric_Type": [*METRICS, *TITLE_METRICS], + }, + "attributes": ["Data_Type", "YOP", "Access_Type"], + }, +} +HOST_TYPES = { + "journals": "eJournal", + "books": "eBook", + "preprints": "Repository", + "repositories": "Repository", + "data": "Data_Repository", +} +REPORTS = { + "pr": { + "name": "Platform Report", + "collections": tuple(HOST_TYPES), + }, + "tr": { + "name": "Title Report", + "collections": ("journals", "books"), + }, + "tr_j3": { + "name": "Journal Usage by Access Type", + "collections": ("journals",), + }, + "tr_b3": { + "name": "Book Usage by Access Type", + "collections": ("books",), + }, + "ir": { + "name": "Item Report", + "collections": tuple(HOST_TYPES), + }, +} From ccc1f1b933a476696265020f5ed37766574782b1 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:28:46 -0300 Subject: [PATCH 10/47] =?UTF-8?q?Normaliza=20identificadores=20dos=20relat?= =?UTF-8?q?=C3=B3rios=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Valida DOI e ISBN e escolhe identificadores proprietários estáveis para títulos e itens. --- counter_api/identifiers.py | 91 +++++++++++++++++++++++++++ counter_api/tests/test_identifiers.py | 23 +++++++ 2 files changed, 114 insertions(+) create mode 100644 counter_api/identifiers.py create mode 100644 counter_api/tests/test_identifiers.py diff --git a/counter_api/identifiers.py b/counter_api/identifiers.py new file mode 100644 index 00000000..192f9ad4 --- /dev/null +++ b/counter_api/identifiers.py @@ -0,0 +1,91 @@ +import re + +from stdnum import isbn + +from counter_api.constants import PROPRIETARY_NAMESPACE + +IDENTIFIER_NAMES = { + "doi": "DOI", + "isbn": "ISBN", + "eisbn": "ISBN", + "print_issn": "Print_ISSN", + "electronic_issn": "Online_ISSN", + "uri": "URI", + "book_id": "Proprietary", + "chapter_id": "Proprietary", + "scielo_issn": "Proprietary", + "pid_v2": "Proprietary", + "pid_v3": "Proprietary", + "pid_generic": "Proprietary", +} +ITEM_ID_FIELDS = { + "doi": ("identifiers.doi",), + "isbn": ("identifiers.isbn", "identifiers.eisbn"), + "print_issn": ("identifiers.print_issn", "scielo_issn"), + "online_issn": ("identifiers.electronic_issn",), + "proprietary": ( + "source_id", + "document_id", + "identifiers.book_id", + "identifiers.chapter_id", + "identifiers.pid_v2", + "identifiers.pid_v3", + "identifiers.pid_generic", + ), +} +DOI_URL_PREFIXES = ( + "https://doi.org/", + "http://doi.org/", + "https://dx.doi.org/", + "http://dx.doi.org/", +) +DOI_PATTERN = re.compile(r"10\.[1-9][0-9]{2}[0-9.]*/.+", re.IGNORECASE) +PROPRIETARY_IDENTIFIER_PRIORITY = { + "pid_generic": 0, + "pid_v3": 1, + "pid_v2": 2, + "chapter_id": 3, + "book_id": 4, + "scielo_issn": 5, +} + + +def counter_identifiers(metadata): + identifiers = metadata.get("identifiers") or {} + result = {} + + ordered = sorted( + identifiers.items(), + key=lambda item: PROPRIETARY_IDENTIFIER_PRIORITY.get( + str(item[0]).lower(), + len(PROPRIETARY_IDENTIFIER_PRIORITY), + ), + ) + + for key, value in ordered: + name = IDENTIFIER_NAMES.get(str(key).lower()) + if not name or not value or name in result: + continue + + value = str(value) + if name == "DOI": + lower_value = value.lower() + for prefix in DOI_URL_PREFIXES: + if lower_value.startswith(prefix): + value = value[len(prefix) :] + break + if not DOI_PATTERN.fullmatch(value): + continue + + if name == "ISBN": + try: + value = isbn.format(isbn.to_isbn13(value)) + except isbn.ValidationError: + continue + + if name == "Proprietary" and ":" not in value: + value = f"{PROPRIETARY_NAMESPACE}:{value}" + + result[name] = value + + return tuple(sorted(result.items())) diff --git a/counter_api/tests/test_identifiers.py b/counter_api/tests/test_identifiers.py new file mode 100644 index 00000000..f8aea529 --- /dev/null +++ b/counter_api/tests/test_identifiers.py @@ -0,0 +1,23 @@ +from django.test import SimpleTestCase + +from counter_api.identifiers import counter_identifiers + + +class CounterIdentifierTests(SimpleTestCase): + def test_normalizes_standard_and_proprietary_identifiers(self): + result = dict( + counter_identifiers( + { + "identifiers": { + "doi": "https://doi.org/10.1234/example", + "isbn": "8575410326", + "pid_v2": "legacy-id", + "pid_generic": "canonical-id", + } + } + ) + ) + + self.assertEqual(result["DOI"], "10.1234/example") + self.assertEqual(result["ISBN"], "978-85-7541-032-5") + self.assertEqual(result["Proprietary"], "SciELO:canonical-id") From d91ac7eef4cebfa4fb3b4eb78268844fc21fef53 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:28:46 -0300 Subject: [PATCH 11/47] =?UTF-8?q?Valida=20par=C3=A2metros=20dos=20relat?= =?UTF-8?q?=C3=B3rios=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aplica filtros e atributos por relatório, respeita as visões padronizadas e limita anos de publicação a 0001–9999. --- counter_api/parameters.py | 186 +++++++++++++++++++++++++++ counter_api/platforms.py | 29 +++++ counter_api/tests/test_parameters.py | 76 +++++++++++ counter_api/yop.py | 61 +++++++++ 4 files changed, 352 insertions(+) create mode 100644 counter_api/parameters.py create mode 100644 counter_api/platforms.py create mode 100644 counter_api/tests/test_parameters.py create mode 100644 counter_api/yop.py diff --git a/counter_api/parameters.py b/counter_api/parameters.py new file mode 100644 index 00000000..6a2c779f --- /dev/null +++ b/counter_api/parameters.py @@ -0,0 +1,186 @@ +from django.conf import settings + +from counter_api.constants import METRICS, STANDARD_VIEWS, TITLE_METRICS +from counter_api.exceptions import CounterAPIError +from counter_api.yop import valid_yop + +FILTERS = { + "data_type": "Data_Type", + "access_type": "Access_Type", + "access_method": "Access_Method", + "metric_type": "Metric_Type", + "yop": "YOP", +} +ATTRIBUTES = { + "exclude_monthly_details": "Exclude_Monthly_Details", + "include_parent_details": "Include_Parent_Details", +} +ATTRIBUTE_NAMES = {"Data_Type", "YOP", "Access_Type", "Access_Method"} +DATA_TYPES = { + "Article", + "Book", + "Book_Segment", + "Dataset", + "Journal", + "Reference_Work", + "Unspecified", +} +FILTER_VALUES = { + "Data_Type": DATA_TYPES, + "Access_Type": {"Controlled", "Free_To_Read", "Open"}, + "Access_Method": {"Regular", "TDM"}, + "Metric_Type": {*METRICS, *TITLE_METRICS}, +} +REPORT_FILTERS = { + "pr": {"Data_Type", "Access_Method", "Metric_Type"}, + "tr": set(FILTERS.values()), + "ir": set(FILTERS.values()), + "tr_j3": set(), + "tr_b3": set(), +} + + +def parse_parameters(query_params, report_id=None): + params = {} + exceptions = [] + unsupported = [] + standard_view = report_id in STANDARD_VIEWS + + attributes_to_show = query_params.get("attributes_to_show") + if attributes_to_show and standard_view: + unsupported.append("attributes_to_show") + elif attributes_to_show: + requested = [item for item in attributes_to_show.split("|") if item] + invalid = [item for item in requested if item not in ATTRIBUTE_NAMES] + if invalid: + exceptions.append( + { + "Code": 3062, + "Message": "Invalid ReportAttribute Value", + "Data": "|".join(invalid), + } + ) + + params["Attributes_To_Show"] = [ + item for item in requested if item in ATTRIBUTE_NAMES + ] + + invalid_filters = [] + allowed_filters = REPORT_FILTERS.get(report_id, set(FILTERS.values())) + + for external, internal in FILTERS.items(): + value = query_params.get(external) + if not value: + continue + if internal not in allowed_filters: + unsupported.append(external) + continue + + requested = [item for item in value.split("|") if item] + if internal == "YOP": + valid = [item for item in requested if valid_yop(item)] + else: + valid = [item for item in requested if item in FILTER_VALUES[internal]] + + invalid_filters.extend(item for item in requested if item not in valid) + if valid: + params[internal] = valid + + if invalid_filters: + exceptions.append( + { + "Code": 3060, + "Message": "Invalid ReportFilter Value", + "Data": "|".join(invalid_filters), + } + ) + + item_id = query_params.get("item_id") + if item_id and report_id in {"tr", "ir"}: + params["Item_ID"] = item_id + elif item_id: + unsupported.append("item_id") + + for external, internal in ATTRIBUTES.items(): + value = query_params.get(external) + if value is None: + continue + if standard_view: + unsupported.append(external) + continue + if value.lower() not in {"true", "false"}: + exceptions.append( + { + "Code": 3062, + "Message": "Invalid ReportAttribute Value", + "Data": external, + } + ) + continue + + enabled = value.lower() == "true" + if internal == "Include_Parent_Details" and enabled and report_id != "ir": + exceptions.append( + { + "Code": 3062, + "Message": "Invalid ReportAttribute Value", + "Data": external, + } + ) + continue + + params[internal] = enabled + + output_format = query_params.get("format") + if output_format and output_format not in {"json", "xlsx", "tsv"}: + exceptions.append( + { + "Code": 3060, + "Message": "Invalid ReportFilter Value", + "Data": "format", + } + ) + + recognized = { + "api_key", + "customer_id", + "platform", + "begin_date", + "end_date", + "format", + "item_id", + "attributes_to_show", + *FILTERS, + *ATTRIBUTES, + } + unsupported = sorted(set(unsupported) | (set(query_params) - recognized)) + if unsupported: + exceptions.append( + { + "Code": 3050, + "Message": "Parameter Not Recognized in this Context", + "Data": "|".join(unsupported), + } + ) + + params["parameter_exceptions"] = exceptions + + return params + + +def validate_customer(query_params): + customer_id = query_params.get("customer_id") + if customer_id and customer_id != settings.COUNTER_CUSTOMER_ID: + raise CounterAPIError( + 2010, + "Requestor is Not Authorized to Access Usage for Institution", + 403, + customer_id, + ) + + +def report_parameters(report_id, params): + result = dict(params) + result.update(STANDARD_VIEWS.get(report_id, {}).get("filters") or {}) + + return result diff --git a/counter_api/platforms.py b/counter_api/platforms.py new file mode 100644 index 00000000..c0f6d159 --- /dev/null +++ b/counter_api/platforms.py @@ -0,0 +1,29 @@ +from collection.models import Collection +from counter_api.constants import HOST_TYPES +from counter_api.exceptions import insufficient_information + + +def get_platform(value): + if not value: + raise insufficient_information("platform is required") + + platform = Collection.objects.filter(acron3=value, is_active=True).first() + if not platform or platform.collection_type not in HOST_TYPES: + raise insufficient_information("platform is not recognized") + return platform + + +def list_platforms(): + platforms = Collection.objects.filter( + collection_type__in=HOST_TYPES, + is_active=True, + ).order_by("main_name") + + return [ + { + "Platform_Parameter": platform.acron3, + "Platform_Name": platform.main_name or platform.acron3, + "Platform_Description": platform.domain or "", + } + for platform in platforms + ] diff --git a/counter_api/tests/test_parameters.py b/counter_api/tests/test_parameters.py new file mode 100644 index 00000000..7050a403 --- /dev/null +++ b/counter_api/tests/test_parameters.py @@ -0,0 +1,76 @@ +from django.test import SimpleTestCase + +from counter_api.parameters import parse_parameters + + +class ReportParameterTests(SimpleTestCase): + def test_invalid_filter_values_are_ignored_without_discarding_valid_values(self): + params = parse_parameters( + { + "metric_type": "Total_Item_Requests|Inexistente", + "access_method": "Inválido", + "yop": "2024|2025-2023", + }, + "tr", + ) + + self.assertEqual(params["Metric_Type"], ["Total_Item_Requests"]) + self.assertEqual(params["YOP"], ["2024"]) + self.assertNotIn("Access_Method", params) + self.assertEqual( + params["parameter_exceptions"], + [ + { + "Code": 3060, + "Message": "Invalid ReportFilter Value", + "Data": "Inválido|Inexistente|2025-2023", + } + ], + ) + + def test_filters_outside_report_context_are_ignored(self): + params = parse_parameters( + {"access_type": "Open", "metric_type": "Total_Item_Requests"}, + "pr", + ) + + self.assertEqual(params["Metric_Type"], ["Total_Item_Requests"]) + self.assertNotIn("Access_Type", params) + self.assertEqual(params["parameter_exceptions"][0]["Code"], 3050) + self.assertEqual(params["parameter_exceptions"][0]["Data"], "access_type") + + def test_standard_view_keeps_fixed_filters(self): + params = parse_parameters( + { + "data_type": "Book", + "item_id": "Print_ISSN:1234-5678", + "attributes_to_show": "YOP", + "exclude_monthly_details": "true", + }, + "tr_j3", + ) + + self.assertNotIn("Data_Type", params) + self.assertNotIn("Item_ID", params) + self.assertNotIn("Attributes_To_Show", params) + self.assertNotIn("Exclude_Monthly_Details", params) + self.assertEqual(params["parameter_exceptions"][0]["Code"], 3050) + self.assertEqual( + params["parameter_exceptions"][0]["Data"], + "attributes_to_show|data_type|exclude_monthly_details|item_id", + ) + + def test_yop_uses_counter_special_values_as_limits(self): + params = parse_parameters( + {"yop": "0000|0001|2020-2024|9999|9999-9999|9999-0001"}, + "tr", + ) + + self.assertEqual( + params["YOP"], + ["0001", "2020-2024", "9999", "9999-9999"], + ) + self.assertEqual( + params["parameter_exceptions"][0]["Data"], + "0000|9999-0001", + ) diff --git a/counter_api/yop.py b/counter_api/yop.py new file mode 100644 index 00000000..f1b22a25 --- /dev/null +++ b/counter_api/yop.py @@ -0,0 +1,61 @@ +def valid_yop(value): + if len(value) == 4 and value.isdigit(): + return 1 <= int(value) <= 9999 + + if ( + len(value) != 9 + or value[4] != "-" + or not value[:4].isdigit() + or not value[5:].isdigit() + ): + return False + + start = int(value[:4]) + end = int(value[5:]) + + return 1 <= start <= end <= 9999 + + +def yop_filter(values): + if not values or any(not valid_yop(value) for value in values): + return None + + years = [] + for value in values: + if "-" not in value: + years.append({"term": {"publication_year": int(value)}}) + continue + + years.append( + { + "range": { + "publication_year": { + "gte": int(value[:4]), + "lte": int(value[5:]), + } + } + } + ) + + if any(value.startswith("0001") for value in values): + years.append({"bool": {"must_not": {"exists": {"field": "publication_year"}}}}) + + return {"bool": {"should": years, "minimum_should_match": 1}} + + +def matches_yop(publication_year, values): + try: + year = int(publication_year) + except (TypeError, ValueError): + year = 1 + + if year < 1 or year > 9999: + year = 1 + + for value in values: + if "-" not in value and year == int(value): + return True + if "-" in value and int(value[:4]) <= year <= int(value[5:]): + return True + + return False From b063ba4d16d68ddcc691ecf4860f3f93d0ca09aa Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:10 -0300 Subject: [PATCH 12/47] =?UTF-8?q?Registra=20a=20conclus=C3=A3o=20mensal=20?= =?UTF-8?q?no=20OpenSearch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mantém uma máscara dos dias exportados por coleção e mês em um índice operacional separado das métricas. --- metrics/opensearch/mappings.py | 10 +++ metrics/opensearch/month_status.py | 64 +++++++++++++++++++ metrics/opensearch/names.py | 6 ++ metrics/tests/opensearch/test_month_status.py | 34 ++++++++++ metrics/tests/opensearch/test_names.py | 2 + 5 files changed, 116 insertions(+) create mode 100644 metrics/opensearch/month_status.py create mode 100644 metrics/tests/opensearch/test_month_status.py diff --git a/metrics/opensearch/mappings.py b/metrics/opensearch/mappings.py index ad10e7a9..f5f22d9c 100644 --- a/metrics/opensearch/mappings.py +++ b/metrics/opensearch/mappings.py @@ -67,6 +67,16 @@ }, } +MONTH_STATUS_INDEX_MAPPINGS = { + "dynamic": False, + "properties": { + "collection": {"type": "keyword"}, + "month": {"type": "keyword"}, + "day_mask": {"type": "long"}, + "complete": {"type": "boolean"}, + }, +} + SOURCE_INDEX_MAPPINGS = { "dynamic": False, "properties": { diff --git a/metrics/opensearch/month_status.py b/metrics/opensearch/month_status.py new file mode 100644 index 00000000..528480dd --- /dev/null +++ b/metrics/opensearch/month_status.py @@ -0,0 +1,64 @@ +import calendar + +from django.conf import settings + +from metrics.opensearch.mappings import MONTH_STATUS_INDEX_MAPPINGS +from metrics.opensearch.names import generate_month_status_index_name + +MARK_DAY_SCRIPT = """ +ctx._source.day_mask = (ctx._source.day_mask ?: 0L) | params.day_mask; +ctx._source.complete = ctx._source.day_mask == params.complete_mask; +""" + + +def _month_values(month_date): + month = month_date.strftime("%Y-%m") + days = calendar.monthrange(month_date.year, month_date.month)[1] + + return month, (1 << days) - 1 + + +def mark_days_exported(search_client, collection, month_date, day_mask): + index_name = generate_month_status_index_name(settings.OPENSEARCH_INDEX_NAME) + search_client.create_alias_if_not_exists(index_name, MONTH_STATUS_INDEX_MAPPINGS) + + month, complete_mask = _month_values(month_date) + + search_client.client.update( + index=index_name, + id=f"{collection}:{month}", + body={ + "script": { + "source": MARK_DAY_SCRIPT, + "params": {"day_mask": day_mask, "complete_mask": complete_mask}, + }, + "scripted_upsert": True, + "upsert": { + "collection": collection, + "month": month, + "day_mask": 0, + "complete": False, + }, + }, + retry_on_conflict=3, + refresh="wait_for", + ) + + +def replace_month_status(search_client, collection, month_date, day_mask): + index_name = generate_month_status_index_name(settings.OPENSEARCH_INDEX_NAME) + search_client.create_alias_if_not_exists(index_name, MONTH_STATUS_INDEX_MAPPINGS) + + month, complete_mask = _month_values(month_date) + + search_client.client.index( + index=index_name, + id=f"{collection}:{month}", + body={ + "collection": collection, + "month": month, + "day_mask": day_mask, + "complete": day_mask == complete_mask, + }, + refresh="wait_for", + ) diff --git a/metrics/opensearch/names.py b/metrics/opensearch/names.py index 7a6d5c2a..7df3aa2f 100644 --- a/metrics/opensearch/names.py +++ b/metrics/opensearch/names.py @@ -28,6 +28,12 @@ def generate_month_index_name(index_prefix, collection): return f"{index_prefix}_monthly_{collection}" +def generate_month_status_index_name(index_prefix): + if not index_prefix or not isinstance(index_prefix, str): + raise ValueError("Index prefix must be a non-empty string.") + return f"{index_prefix}_month_status" + + def generate_analytics_index_name(index_prefix, collection): _validate_index_inputs(index_prefix, collection) return f"{index_prefix}_yearly_analytics_{collection}" diff --git a/metrics/tests/opensearch/test_month_status.py b/metrics/tests/opensearch/test_month_status.py new file mode 100644 index 00000000..39e99aae --- /dev/null +++ b/metrics/tests/opensearch/test_month_status.py @@ -0,0 +1,34 @@ +from datetime import date +from unittest.mock import Mock + +from django.test import SimpleTestCase, override_settings + +from metrics.opensearch.month_status import mark_days_exported, replace_month_status + + +@override_settings(OPENSEARCH_INDEX_NAME="usage") +class MonthStatusTests(SimpleTestCase): + def test_marks_month_complete_only_with_every_day(self): + search_client = Mock() + all_days = (1 << 31) - 1 + + mark_days_exported(search_client, "scl", date(2024, 1, 1), all_days) + + search_client.create_alias_if_not_exists.assert_called_once() + call = search_client.client.update.call_args + self.assertEqual(call.kwargs["index"], "usage_month_status") + self.assertEqual(call.kwargs["id"], "scl:2024-01") + self.assertEqual(call.kwargs["body"]["script"]["params"]["day_mask"], all_days) + self.assertEqual( + call.kwargs["body"]["script"]["params"]["complete_mask"], all_days + ) + self.assertEqual(call.kwargs["refresh"], "wait_for") + + def test_replaces_status_when_rebuilding_month(self): + search_client = Mock() + + replace_month_status(search_client, "books", date(2024, 2, 1), (1 << 29) - 1) + + body = search_client.client.index.call_args.kwargs["body"] + self.assertEqual(body["month"], "2024-02") + self.assertTrue(body["complete"]) diff --git a/metrics/tests/opensearch/test_names.py b/metrics/tests/opensearch/test_names.py index f180f717..aa2ba6bb 100644 --- a/metrics/tests/opensearch/test_names.py +++ b/metrics/tests/opensearch/test_names.py @@ -3,6 +3,7 @@ generate_initial_index_name, generate_metadata_alias, generate_month_index_name, + generate_month_status_index_name, generate_rollover_index_name, generate_yearly_write_alias, ) @@ -10,6 +11,7 @@ def test_metric_aliases_are_stable_per_collection(): assert generate_month_index_name("usage", "scl") == "usage_monthly_scl" + assert generate_month_status_index_name("usage") == "usage_month_status" assert generate_analytics_index_name("usage", "scl") == ( "usage_yearly_analytics_scl" ) From 05db63f773bbc39ab51dbb526c9c9fd7ce94653c Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:10 -0300 Subject: [PATCH 13/47] =?UTF-8?q?Marca=20dias=20conclu=C3=ADdos=20ap=C3=B3?= =?UTF-8?q?s=20a=20exporta=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Atualiza a cobertura mensal somente depois de tornar consultáveis as métricas mensais e analíticas do dia. --- metrics/services/daily_metric_exports.py | 24 ++++++++++++++ .../services/test_daily_metric_exports.py | 31 ++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/metrics/services/daily_metric_exports.py b/metrics/services/daily_metric_exports.py index 52965f3a..f27c72bf 100644 --- a/metrics/services/daily_metric_exports.py +++ b/metrics/services/daily_metric_exports.py @@ -2,8 +2,15 @@ import resource from time import monotonic +from django.conf import settings + from metrics.models import DailyMetricJob from metrics.opensearch.client import OpenSearchUsageClient +from metrics.opensearch.month_status import mark_days_exported +from metrics.opensearch.names import ( + generate_analytics_index_name, + generate_month_index_name, +) from metrics.services.export import ( daily_metric_payload_exists, export_daily_metric_payload, @@ -74,6 +81,23 @@ def _export_payload(job): search_client=search_client, job=job, ) + monthly = generate_month_index_name( + settings.OPENSEARCH_INDEX_NAME, job.collection.acron3 + ) + analytics = generate_analytics_index_name( + settings.OPENSEARCH_INDEX_NAME, job.collection.acron3 + ) + search_client.client.indices.refresh( + index=f"{monthly},{analytics}", + ignore_unavailable=True, + ) + mark_days_exported( + search_client, + job.collection.acron3, + job.access_date, + 1 << (job.access_date.day - 1), + ) + logging.info( "Daily metric job %s OpenSearch export completed in %.3f seconds; " "peak RSS %.1f MiB.", diff --git a/metrics/tests/services/test_daily_metric_exports.py b/metrics/tests/services/test_daily_metric_exports.py index 58a1a50a..0032f270 100644 --- a/metrics/tests/services/test_daily_metric_exports.py +++ b/metrics/tests/services/test_daily_metric_exports.py @@ -7,7 +7,7 @@ from django.test import SimpleTestCase, override_settings from metrics.services import daily_payloads -from metrics.services.daily_metric_exports import _ensure_payload +from metrics.services.daily_metric_exports import _ensure_payload, _export_payload from metrics.services.export import export_daily_metric_payload @@ -197,6 +197,35 @@ def test_continuous_collection_routes_existing_documents_before_rollover(self): ["usage_monthly_scl", "usage_yearly_analytics_scl"], ) + @patch("metrics.services.daily_metric_exports.mark_days_exported") + @patch("metrics.services.daily_metric_exports.export_daily_metric_payload") + @patch("metrics.services.daily_metric_exports.OpenSearchUsageClient") + def test_marks_day_after_facts_are_visible( + self, + client_class, + export_payload, + mark_exported, + ): + search_client = client_class.return_value + search_client.ping.return_value = True + + _export_payload(self.job) + + export_payload.assert_called_once_with( + search_client=search_client, + job=self.job, + ) + search_client.client.indices.refresh.assert_called_once_with( + index="usage_monthly_scl,usage_yearly_analytics_scl", + ignore_unavailable=True, + ) + mark_exported.assert_called_once_with( + search_client, + "scl", + date(2026, 8, 25), + 1 << 24, + ) + @patch("metrics.services.daily_metric_exports.fetch_required_resources") @patch("metrics.services.daily_metric_exports.build_daily_metric_job_payload") def test_resume_uses_persisted_payload_without_parsing( From a0587c856d03c2c66b5b8a3c9ae7bdaf84d42451 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:10 -0300 Subject: [PATCH 14/47] =?UTF-8?q?Registra=20os=20meses=20da=20migra=C3=A7?= =?UTF-8?q?=C3=A3o=20hist=C3=B3rica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Marca em uma única operação os dias processados e vazios depois de importar e atualizar os dois conjuntos de métricas. --- metrics/legacy_matomo/importer.py | 29 +++++++++++++-- .../services/test_legacy_matomo_importer.py | 35 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/metrics/legacy_matomo/importer.py b/metrics/legacy_matomo/importer.py index a40e2d13..0afa7505 100644 --- a/metrics/legacy_matomo/importer.py +++ b/metrics/legacy_matomo/importer.py @@ -1,5 +1,6 @@ import gzip import json +from datetime import date from functools import partial from itertools import islice @@ -13,6 +14,7 @@ build_counter_increment_action, ) from metrics.legacy_matomo.routing import build_usage_index_target +from metrics.opensearch.month_status import mark_days_exported from metrics.opensearch.names import generate_yearly_write_alias DOCUMENT_INDEX_LOOKUP_BATCH_SIZE = 1000 @@ -267,8 +269,10 @@ def import_manifest( stop_controller=None, ): config = _collection_config(collection) - return { - dataset: import_dataset( + imported = {} + + for dataset in ("counter", "analytics"): + imported[dataset] = import_dataset( search_client, collection, config, @@ -278,5 +282,24 @@ def import_manifest( progress_interval=progress_interval, stop_controller=stop_controller, ) + + targets = [ + _dataset_target(collection, config, manifest, dataset) for dataset in ("counter", "analytics") - } + ] + search_client.client.indices.refresh( + index=",".join(target["read_alias"] for target in targets), + ignore_unavailable=True, + ) + + month_date = date.fromisoformat(f"{manifest['month']}-01") + days = manifest["source_days"] + manifest.get("empty_days", []) + day_mask = sum(1 << (date.fromisoformat(day).day - 1) for day in days) + mark_days_exported( + search_client, + collection.acron3, + month_date, + day_mask, + ) + + return imported diff --git a/metrics/tests/services/test_legacy_matomo_importer.py b/metrics/tests/services/test_legacy_matomo_importer.py index 4c022efa..7de72c4f 100644 --- a/metrics/tests/services/test_legacy_matomo_importer.py +++ b/metrics/tests/services/test_legacy_matomo_importer.py @@ -1,3 +1,4 @@ +from datetime import date from types import SimpleNamespace from unittest.mock import Mock, patch @@ -171,3 +172,37 @@ def consume(_client, action_items, chunk_size): ], ) self.assertEqual(progress, [("counter", "import", 2)]) + + @override_settings(OPENSEARCH_INDEX_NAME="usage") + @patch("metrics.legacy_matomo.importer.mark_days_exported") + @patch("metrics.legacy_matomo.importer.import_dataset") + def test_import_marks_source_and_empty_days_after_refresh( + self, + import_dataset, + mark_days, + ): + self.manifest["source_days"] = ["2025-08-01", "2025-08-31"] + self.manifest["empty_days"] = ["2025-08-02"] + collection = SimpleNamespace( + acron3="scl", + log_manager_config=SimpleNamespace( + opensearch_partition_strategy="yearly", + opensearch_primary_shards=1, + ), + ) + search_client = Mock() + import_dataset.side_effect = [10, 20] + + result = importer.import_manifest(search_client, collection, self.manifest) + + self.assertEqual(result, {"counter": 10, "analytics": 20}) + search_client.client.indices.refresh.assert_called_once_with( + index="usage_monthly_scl,usage_yearly_analytics_scl", + ignore_unavailable=True, + ) + mark_days.assert_called_once_with( + search_client, + "scl", + date(2025, 8, 1), + (1 << 0) | (1 << 1) | (1 << 30), + ) From bae0273bc5c726baa27ac86d508e9c5008d7f784 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:10 -0300 Subject: [PATCH 15/47] =?UTF-8?q?Reconstr=C3=B3i=20a=20disponibilidade=20m?= =?UTF-8?q?ensal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oferece comando para recompor o controle de cobertura a partir dos trabalhos diários e da migração histórica. --- .../commands/backfill_counter_month_status.py | 129 ++++++++++++++++++ .../services/test_month_status_command.py | 101 ++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 metrics/management/commands/backfill_counter_month_status.py create mode 100644 metrics/tests/services/test_month_status_command.py diff --git a/metrics/management/commands/backfill_counter_month_status.py b/metrics/management/commands/backfill_counter_month_status.py new file mode 100644 index 00000000..36665963 --- /dev/null +++ b/metrics/management/commands/backfill_counter_month_status.py @@ -0,0 +1,129 @@ +import json +from datetime import date + +from django.core.management.base import BaseCommand, CommandError + +from collection.models import Collection +from counter_api.dates import iter_months +from metrics.legacy_matomo.manifest import load_manifest +from metrics.models import DailyMetricJob +from metrics.opensearch.client import OpenSearchUsageClient +from metrics.opensearch.month_status import replace_month_status + + +class Command(BaseCommand): + help = "Rebuild monthly COUNTER availability from jobs or migration reports." + + def add_arguments(self, parser): + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--collection") + source.add_argument("--report", nargs="+") + parser.add_argument("--start-month") + parser.add_argument("--end-month") + parser.add_argument("--apply", action="store_true") + + def handle(self, *args, **options): + client = OpenSearchUsageClient() if options["apply"] else None + + try: + if options["report"]: + if options["start_month"] or options["end_month"]: + raise CommandError("month arguments cannot be used with reports") + + self._from_reports(options["report"], client) + return + + if not options["start_month"] or not options["end_month"]: + raise CommandError("start-month and end-month are required") + + self._from_jobs( + options["collection"], + options["start_month"], + options["end_month"], + client, + ) + except CommandError: + raise + except ( + OSError, + Collection.DoesNotExist, + KeyError, + TypeError, + ValueError, + ) as exc: + raise CommandError(str(exc)) from exc + + def _from_jobs(self, acronym, start_value, end_value, client): + try: + start = date.fromisoformat(f"{start_value}-01") + end = date.fromisoformat(f"{end_value}-01") + except ValueError as exc: + raise CommandError("months must use YYYY-MM") from exc + + if start > end: + raise CommandError("start-month must not be later than end-month") + + if end.month == 12: + following_month = date(end.year + 1, 1, 1) + else: + following_month = date(end.year, end.month + 1, 1) + + collection = Collection.objects.get(acron3=acronym) + exported_days = DailyMetricJob.objects.filter( + collection=collection, + status=DailyMetricJob.STATUS_EXPORTED, + access_date__gte=start, + access_date__lt=following_month, + ).values_list("access_date", flat=True) + masks = {} + + for access_date in exported_days: + month = access_date.replace(day=1) + masks[month] = masks.get(month, 0) | (1 << (access_date.day - 1)) + + for month in iter_months(start, end): + self._write_status( + client, + collection.acron3, + month, + masks.get(month, 0), + ) + + def _from_reports(self, paths, client): + for path in paths: + with open(path, encoding="utf-8") as source: + report = json.load(source) + + if report.get("status") != "completed": + raise CommandError(f"migration report is not completed: {path}") + if report.get("validation", {}).get("status") != "valid": + raise CommandError(f"migration report is not valid: {path}") + + for dataset in ("counter", "analytics"): + expected = report["validation"][dataset]["documents"] + if report["imported"][dataset] != expected: + raise CommandError(f"migration report is incomplete: {path}") + + manifest = load_manifest(report["manifest"]) + collection = manifest["target"]["collection"] + month = date.fromisoformat(f"{manifest['month']}-01") + if report.get("collection") != collection: + raise CommandError(f"collection differs from manifest: {path}") + if report.get("month") != manifest["month"]: + raise CommandError(f"month differs from manifest: {path}") + + days = manifest["source_days"] + manifest.get("empty_days", []) + day_mask = 0 + for value in days: + access_date = date.fromisoformat(value) + if access_date.replace(day=1) != month: + raise CommandError(f"day outside manifest month: {path}") + day_mask |= 1 << (access_date.day - 1) + + self._write_status(client, collection, month, day_mask) + + def _write_status(self, client, collection, month, day_mask): + if client: + replace_month_status(client, collection, month, day_mask) + + self.stdout.write(f"{collection} {month:%Y-%m} days={day_mask.bit_count()}") diff --git a/metrics/tests/services/test_month_status_command.py b/metrics/tests/services/test_month_status_command.py new file mode 100644 index 00000000..9d721c31 --- /dev/null +++ b/metrics/tests/services/test_month_status_command.py @@ -0,0 +1,101 @@ +import json +import tempfile +from datetime import date +from io import StringIO +from pathlib import Path +from unittest.mock import patch + +from django.core.management import call_command +from django.test import TestCase + +from collection.models import Collection +from metrics.models import DailyMetricJob + + +class BackfillMonthStatusCommandTests(TestCase): + @patch( + "metrics.management.commands.backfill_counter_month_status." + "replace_month_status" + ) + @patch( + "metrics.management.commands.backfill_counter_month_status." + "OpenSearchUsageClient" + ) + def test_uses_only_exported_days(self, client_class, replace_status): + collection = Collection.objects.create(acron3="scl", acron2="sc") + for access_date, status in ( + (date(2025, 1, 1), DailyMetricJob.STATUS_EXPORTED), + (date(2025, 1, 2), DailyMetricJob.STATUS_PENDING), + (date(2025, 1, 31), DailyMetricJob.STATUS_EXPORTED), + ): + DailyMetricJob.objects.create( + collection=collection, + access_date=access_date, + status=status, + ) + + output = StringIO() + call_command( + "backfill_counter_month_status", + collection="scl", + start_month="2025-01", + end_month="2025-01", + apply=True, + stdout=output, + ) + + replace_status.assert_called_once_with( + client_class.return_value, + "scl", + date(2025, 1, 1), + (1 << 0) | (1 << 30), + ) + self.assertIn("days=2", output.getvalue()) + + @patch( + "metrics.management.commands.backfill_counter_month_status." + "replace_month_status" + ) + @patch( + "metrics.management.commands.backfill_counter_month_status." + "OpenSearchUsageClient" + ) + def test_uses_completed_migration_reports(self, client_class, replace_status): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + manifest_path = root / "manifest.json" + report_path = root / "report.json" + manifest = { + "month": "2025-02", + "source_days": ["2025-02-01", "2025-02-28"], + "empty_days": ["2025-02-02"], + "target": {"collection": "scl"}, + } + report = { + "status": "completed", + "manifest": str(manifest_path), + "collection": "scl", + "month": "2025-02", + "validation": { + "status": "valid", + "counter": {"documents": 10}, + "analytics": {"documents": 20}, + }, + "imported": {"counter": 10, "analytics": 20}, + } + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + report_path.write_text(json.dumps(report), encoding="utf-8") + + call_command( + "backfill_counter_month_status", + report=[str(report_path)], + apply=True, + stdout=StringIO(), + ) + + replace_status.assert_called_once_with( + client_class.return_value, + "scl", + date(2025, 2, 1), + (1 << 0) | (1 << 1) | (1 << 27), + ) From 533d6488ff6070219b3b5c209432a272e4de292f Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:10 -0300 Subject: [PATCH 16/47] =?UTF-8?q?Controla=20a=20cobertura=20temporal=20dos?= =?UTF-8?q?=20relat=C3=B3rios=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Separa meses disponíveis, pendentes e antigos usando o índice explícito de conclusão mensal. --- counter_api/availability.py | 79 ++++++++++++++++++++++++++ counter_api/tests/test_availability.py | 39 +++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 counter_api/availability.py create mode 100644 counter_api/tests/test_availability.py diff --git a/counter_api/availability.py b/counter_api/availability.py new file mode 100644 index 00000000..defa5de2 --- /dev/null +++ b/counter_api/availability.py @@ -0,0 +1,79 @@ +from datetime import date +from time import monotonic + +from django.conf import settings +from opensearchpy import NotFoundError + +from counter_api.dates import iter_months +from counter_api.search import search +from metrics.opensearch.client import OpenSearchUsageClient +from metrics.opensearch.names import generate_month_status_index_name + +PAGE_SIZE = 1000 + + +def available_months(platform): + index_name = generate_month_status_index_name(settings.OPENSEARCH_INDEX_NAME) + client = OpenSearchUsageClient().client + deadline = monotonic() + settings.COUNTER_QUERY_BUDGET_SECONDS + current_month = date.today().strftime("%Y-%m") + months = [] + after = None + + while True: + body = { + "size": 0, + "track_total_hits": False, + "query": { + "bool": { + "filter": [ + {"term": {"collection": platform.acron3}}, + {"term": {"complete": True}}, + {"range": {"month": {"lt": current_month}}}, + ] + } + }, + "aggs": { + "months": { + "composite": { + "size": PAGE_SIZE, + "sources": [{"month": {"terms": {"field": "month"}}}], + } + } + }, + } + if after: + body["aggs"]["months"]["composite"]["after"] = after + + try: + response = search(client, index_name, body, deadline) + except NotFoundError: + return [] + + page = response["aggregations"]["months"] + months.extend( + date.fromisoformat(f"{bucket['key']['month']}-01") + for bucket in page["buckets"] + ) + after = page.get("after_key") + if not after: + return months + + +def split_period(platform, begin, end): + requested = list(iter_months(begin, end)) + complete = set(available_months(platform)) + + if not complete: + return [], requested, [] + + first_available = min(complete) + available = [month for month in requested if month in complete] + unavailable = [month for month in requested if month < first_available] + pending = [ + month + for month in requested + if month not in complete and month not in unavailable + ] + + return available, pending, unavailable diff --git a/counter_api/tests/test_availability.py b/counter_api/tests/test_availability.py new file mode 100644 index 00000000..90ea0f9a --- /dev/null +++ b/counter_api/tests/test_availability.py @@ -0,0 +1,39 @@ +from datetime import date +from unittest.mock import Mock, patch + +from django.test import SimpleTestCase + +from counter_api.availability import available_months, split_period + + +class AvailabilityTests(SimpleTestCase): + @patch("counter_api.availability.OpenSearchUsageClient") + def test_reads_only_complete_months_from_status_index(self, client_class): + client = client_class.return_value.client + client.search.return_value = { + "aggregations": { + "months": { + "buckets": [ + {"key": {"month": "2024-01"}}, + {"key": {"month": "2024-03"}}, + ] + } + } + } + + months = available_months(Mock(acron3="scl")) + + self.assertEqual(months, [date(2024, 1, 1), date(2024, 3, 1)]) + filters = client.search.call_args.kwargs["body"]["query"]["bool"]["filter"] + self.assertIn({"term": {"collection": "scl"}}, filters) + self.assertIn({"term": {"complete": True}}, filters) + + @patch("counter_api.availability.available_months") + def test_splits_missing_months_without_treating_gaps_as_no_usage(self, months): + months.return_value = [date(2024, 1, 1), date(2024, 3, 1)] + + result = split_period(Mock(), date(2023, 12, 1), date(2024, 3, 31)) + + self.assertEqual(result[0], [date(2024, 1, 1), date(2024, 3, 1)]) + self.assertEqual(result[1], [date(2024, 2, 1)]) + self.assertEqual(result[2], [date(2023, 12, 1)]) From 47f40719a845c97c19db3e37c25785c341d6c673 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:39 -0300 Subject: [PATCH 17/47] Protege consultas ao OpenSearch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aplica orçamento de execução, trata falhas de fragmentos e compartilha a paginação de agregações compostas. --- counter_api/search.py | 52 ++++++++++++++++++++++++++++++++ counter_api/tests/test_search.py | 20 ++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 counter_api/search.py create mode 100644 counter_api/tests/test_search.py diff --git a/counter_api/search.py b/counter_api/search.py new file mode 100644 index 00000000..96ded8df --- /dev/null +++ b/counter_api/search.py @@ -0,0 +1,52 @@ +from time import monotonic + +from opensearchpy import NotFoundError +from opensearchpy.exceptions import OpenSearchException + +from counter_api.exceptions import service_unavailable + + +def remaining_time(deadline, limit=None): + remaining = deadline - monotonic() + if remaining <= 0: + raise service_unavailable() + if limit is not None: + return min(remaining, limit) + return remaining + + +def search(client, index_name, body, deadline, limit=None): + timeout = remaining_time(deadline, limit) + body["timeout"] = f"{max(1, int(timeout * 1000))}ms" + try: + response = client.search( + index=index_name, + body=body, + request_timeout=timeout, + ) + except NotFoundError: + raise + except OpenSearchException: + raise service_unavailable() + + if response.get("timed_out") or response.get("_shards", {}).get("failed"): + raise service_unavailable() + + return response + + +def composite_pages(client, index_name, body, deadline): + after = None + + while True: + if after: + body["aggs"]["rows"]["composite"]["after"] = after + + response = search(client, index_name, body, deadline) + page = response["aggregations"]["rows"] + + yield page.get("buckets", []) + + after = page.get("after_key") + if not after: + return diff --git a/counter_api/tests/test_search.py b/counter_api/tests/test_search.py new file mode 100644 index 00000000..89d2a59c --- /dev/null +++ b/counter_api/tests/test_search.py @@ -0,0 +1,20 @@ +from time import monotonic +from unittest.mock import Mock + +from django.test import SimpleTestCase +from opensearchpy.exceptions import OpenSearchException + +from counter_api.exceptions import CounterAPIError +from counter_api.search import search + + +class SearchTests(SimpleTestCase): + def test_converts_opensearch_failures_to_counter_exception(self): + client = Mock() + client.search.side_effect = OpenSearchException("connection failed") + + with self.assertRaises(CounterAPIError) as raised: + search(client, "usage_month_status", {}, monotonic() + 1) + + self.assertEqual(raised.exception.code, 1000) + self.assertEqual(raised.exception.status_code, 503) From 35197ed90ed67d0f25537218d6d762be1e9cb802 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:39 -0300 Subject: [PATCH 18/47] =?UTF-8?q?Carrega=20metadados=20dos=20relat=C3=B3ri?= =?UTF-8?q?os=20em=20lotes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Busca fontes e documentos nos índices próprios sem enriquecer os índices enxutos de métricas. --- counter_api/metadata.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 counter_api/metadata.py diff --git a/counter_api/metadata.py b/counter_api/metadata.py new file mode 100644 index 00000000..ade95e6d --- /dev/null +++ b/counter_api/metadata.py @@ -0,0 +1,34 @@ +from django.conf import settings +from opensearchpy import NotFoundError + +from counter_api.constants import METADATA_BATCH_SIZE +from counter_api.search import remaining_time +from metrics.opensearch.names import generate_metadata_alias + + +def fetch_metadata(client, entity, keys, deadline): + if not keys: + return {} + + index_name = generate_metadata_alias(settings.OPENSEARCH_INDEX_NAME, entity) + documents = {} + keys = sorted(set(keys)) + + for start in range(0, len(keys), METADATA_BATCH_SIZE): + remaining = remaining_time(deadline) + + batch = keys[start : start + METADATA_BATCH_SIZE] + try: + response = client.mget( + index=index_name, + body={"ids": batch}, + request_timeout=remaining, + ) + except NotFoundError: + return documents + + for document in response.get("docs", []): + if document.get("found"): + documents[document["_id"]] = document["_source"] + + return documents From 86775b939f7d3680bac1de33e6fbda7a3171b92c Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:39 -0300 Subject: [PATCH 19/47] =?UTF-8?q?Consulta=20as=20m=C3=A9tricas=20dos=20rel?= =?UTF-8?q?at=C3=B3rios=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agrega no OpenSearch, resolve identificadores por coleção e aplica filtros antes da transferência dos resultados. --- counter_api/query.py | 246 ++++++++++++++++++++++++++++++++ counter_api/tests/test_query.py | 163 +++++++++++++++++++++ 2 files changed, 409 insertions(+) create mode 100644 counter_api/query.py create mode 100644 counter_api/tests/test_query.py diff --git a/counter_api/query.py b/counter_api/query.py new file mode 100644 index 00000000..e741417a --- /dev/null +++ b/counter_api/query.py @@ -0,0 +1,246 @@ +from time import monotonic + +from django.conf import settings +from opensearchpy import NotFoundError + +from counter_api.constants import COMPOSITE_PAGE_SIZE +from counter_api.exceptions import incongruous_filter, invalid_filter +from counter_api.identifiers import ITEM_ID_FIELDS +from counter_api.metadata import fetch_metadata +from counter_api.search import composite_pages, search +from counter_api.yop import yop_filter +from metrics.opensearch.client import OpenSearchUsageClient +from metrics.opensearch.names import generate_metadata_alias, generate_month_index_name + +ITEM_GROUP_FIELDS = ( + "source_key", + "document_key", + "metric_scope", + "data_type", + "parent_data_type", + "access_type", + "access_method", + "publication_year", +) +PLATFORM_GROUP_FIELDS = ("metric_scope", "access_type", "access_method") +TITLE_GROUP_FIELDS = ( + "source_key", + "metric_scope", + "data_type", + "parent_data_type", + "access_type", + "access_method", + "publication_year", +) + + +class ReportQuery: + def __init__(self, client=None): + usage_client = client or OpenSearchUsageClient() + self.client = usage_client.client + self.deadline = monotonic() + settings.COUNTER_QUERY_BUDGET_SECONDS + + def fetch(self, platform, months, report_id=None, params=None): + if not months: + return + + for buckets in self._fetch_buckets(platform.acron3, months, report_id, params): + source_keys = {bucket["key"].get("source_key") for bucket in buckets} + document_keys = {bucket["key"].get("document_key") for bucket in buckets} + source_keys.discard(None) + document_keys.discard(None) + + sources = fetch_metadata( + self.client, + "sources", + source_keys, + self.deadline, + ) + + documents = fetch_metadata( + self.client, + "documents", + document_keys, + self.deadline, + ) + + for bucket in buckets: + key = bucket["key"] + yield ( + bucket, + sources.get(key.get("source_key"), {}), + documents.get(key.get("document_key"), {}), + ) + + def resolve_item_id(self, platform, report_id, item_id): + field, key = self.resolve_item_filter(platform, report_id, item_id) + + return key + + def resolve_item_filter(self, platform, report_id, item_id): + namespace, separator, value = item_id.partition(":") + if not separator or not value: + raise invalid_filter("item_id") + + fields = ITEM_ID_FIELDS.get(namespace.lower()) + if not fields: + raise invalid_filter("item_id") + + if report_id.startswith("tr"): + entities = ("sources",) + elif namespace.lower() in {"print_issn", "online_issn", "isbn"}: + entities = ("sources",) + elif namespace.lower() == "proprietary": + entities = ("documents", "sources") + else: + entities = ("documents",) + + hits = [] + for entity in entities: + direct_field = "source_id" if entity == "sources" else "document_id" + candidates = [ + field + for field in fields + if field != "source_id" and field != "document_id" + ] + if namespace.lower() == "proprietary": + candidates.append(direct_field) + + index_name = generate_metadata_alias(settings.OPENSEARCH_INDEX_NAME, entity) + body = { + "size": 2, + "_source": False, + "track_total_hits": False, + "query": { + "bool": { + "filter": [{"term": {"collection": platform.acron3}}], + "should": [ + {"term": {candidate: value}} for candidate in candidates + ], + "minimum_should_match": 1, + } + }, + } + try: + response = search( + self.client, + index_name, + body, + self.deadline, + limit=5, + ) + except NotFoundError: + response = {"hits": {"hits": []}} + + key_field = "source_key" if entity == "sources" else "document_key" + hits.extend((key_field, hit["_id"]) for hit in response["hits"]["hits"]) + + if not hits: + raise invalid_filter("item_id") + if len(hits) > 1: + raise incongruous_filter("item_id") + + return hits[0] + + def _fetch_buckets(self, collection, months, report_id, params): + index_name = generate_month_index_name( + settings.OPENSEARCH_INDEX_NAME, + collection, + ) + body = self._query_body(months, None, report_id, params) + + try: + yield from composite_pages( + self.client, + index_name, + body, + self.deadline, + ) + except NotFoundError: + return + + def _query_body(self, months, after, report_id, params=None): + params = params or {} + fields = ITEM_GROUP_FIELDS + if report_id == "pr": + fields = PLATFORM_GROUP_FIELDS + elif report_id and report_id.startswith("tr"): + fields = TITLE_GROUP_FIELDS + + sources = [] + for field in fields: + sources.append( + { + field: { + "terms": { + "field": field, + "missing_bucket": True, + } + } + } + ) + + sources.insert( + 2, + { + "month": { + "date_histogram": { + "field": "month", + "calendar_interval": "1M", + "format": "yyyy-MM", + } + } + }, + ) + + composite = { + "size": COMPOSITE_PAGE_SIZE, + "sources": sources, + } + if after: + composite["after"] = after + + filters = [{"terms": {"month": [month.strftime("%Y-%m") for month in months]}}] + for parameter, field in ( + ("Access_Type", "access_type"), + ("Access_Method", "access_method"), + ): + if params.get(parameter): + filters.append({"terms": {field: params[parameter]}}) + + if report_id == "ir" and params.get("Data_Type"): + filters.append({"terms": {"data_type": params["Data_Type"]}}) + if report_id == "ir": + filters.append({"term": {"metric_scope": "item"}}) + + if params.get("Item_Key"): + field = params.get("Item_Key_Field") + if not field: + field = "source_key" if report_id.startswith("tr") else "document_key" + filters.append({"term": {field: params["Item_Key"]}}) + + if report_id in {"tr", "tr_j3", "tr_b3", "ir"} and params.get("YOP"): + clause = yop_filter(params["YOP"]) + if clause: + filters.append(clause) + + return { + "size": 0, + "track_total_hits": False, + "query": {"bool": {"filter": filters}}, + "aggs": { + "rows": { + "composite": composite, + "aggs": { + "total_requests": {"sum": {"field": "total_requests"}}, + "total_investigations": { + "sum": {"field": "total_investigations"} + }, + "unique_requests": {"sum": {"field": "unique_requests"}}, + "unique_investigations": { + "sum": {"field": "unique_investigations"} + }, + }, + } + }, + } diff --git a/counter_api/tests/test_query.py b/counter_api/tests/test_query.py new file mode 100644 index 00000000..ef92764d --- /dev/null +++ b/counter_api/tests/test_query.py @@ -0,0 +1,163 @@ +from datetime import date +from unittest.mock import Mock, patch + +from django.test import TestCase + +from counter_api.exceptions import CounterAPIError +from counter_api.query import ReportQuery + + +class ReportQueryTests(TestCase): + def test_item_id_resolution_is_scoped_to_collection(self): + client = Mock() + client.search.return_value = {"hits": {"hits": [{"_id": "article-key"}]}} + query = ReportQuery(Mock(client=client)) + + key = query.resolve_item_id(Mock(acron3="scl"), "ir", "DOI:10.1234/example") + body = client.search.call_args.kwargs["body"] + report_body = query._query_body( + [date(2024, 1, 1)], + None, + "ir", + {"Item_Key": key}, + ) + + self.assertEqual(key, "article-key") + self.assertIn({"term": {"collection": "scl"}}, body["query"]["bool"]["filter"]) + self.assertIn( + {"term": {"document_key": "article-key"}}, + report_body["query"]["bool"]["filter"], + ) + + client.search.return_value = { + "hits": {"hits": [{"_id": "one"}, {"_id": "two"}]} + } + with self.assertRaises(CounterAPIError): + query.resolve_item_id(Mock(acron3="scl"), "ir", "DOI:10.1234/example") + + def test_partial_shard_failure_does_not_return_report(self): + client = Mock() + client.search.return_value = {"_shards": {"failed": 1}} + query = ReportQuery(Mock(client=client)) + + with self.assertRaises(CounterAPIError): + list(query.fetch(Mock(acron3="scl"), [date(2024, 1, 1)])) + + def test_platform_query_does_not_group_by_items(self): + query = ReportQuery(Mock(client=Mock())) + + body = query._query_body([date(2024, 1, 1)], None, "pr") + sources = body["aggs"]["rows"]["composite"]["sources"] + fields = {next(iter(source)) for source in sources} + + self.assertNotIn("source_key", fields) + self.assertNotIn("document_key", fields) + self.assertIn("month", fields) + + def test_query_applies_access_filters_before_aggregation(self): + query = ReportQuery(Mock(client=Mock())) + + body = query._query_body( + [date(2024, 1, 1)], + None, + "tr_j3", + {"Access_Type": ["Open"], "Access_Method": ["Regular"]}, + ) + filters = body["query"]["bool"]["filter"] + + self.assertIn({"terms": {"access_type": ["Open"]}}, filters) + self.assertIn({"terms": {"access_method": ["Regular"]}}, filters) + self.assertFalse(body["track_total_hits"]) + + def test_query_applies_yop_list_and_range_in_opensearch(self): + query = ReportQuery(Mock(client=Mock())) + + body = query._query_body( + [date(2024, 1, 1)], + None, + "ir", + {"YOP": ["2020", "2022-2023"], "Data_Type": ["Article"]}, + ) + filters = body["query"]["bool"]["filter"] + + self.assertIn({"terms": {"data_type": ["Article"]}}, filters) + self.assertIn({"term": {"metric_scope": "item"}}, filters) + self.assertIn( + { + "bool": { + "should": [ + {"term": {"publication_year": 2020}}, + {"range": {"publication_year": {"gte": 2022, "lte": 2023}}}, + ], + "minimum_should_match": 1, + } + }, + filters, + ) + + @patch("counter_api.metadata.METADATA_BATCH_SIZE", 1) + def test_composite_query_reads_every_page_and_metadata_in_batches(self): + client = Mock() + client.search.side_effect = [ + { + "aggregations": { + "rows": { + "buckets": [ + { + "key": { + "source_key": "source-1", + "document_key": "document-1", + } + } + ], + "after_key": {"document_key": "document-1"}, + } + } + }, + { + "aggregations": { + "rows": { + "buckets": [ + { + "key": { + "source_key": "source-2", + "document_key": "document-2", + } + } + ] + } + } + }, + ] + client.mget.side_effect = [ + { + "docs": [ + {"_id": "source-1", "found": True, "_source": {"title": "A"}}, + ] + }, + { + "docs": [ + {"_id": "document-1", "found": True, "_source": {"title": "C"}}, + ] + }, + { + "docs": [ + {"_id": "source-2", "found": True, "_source": {"title": "B"}}, + ] + }, + { + "docs": [ + {"_id": "document-2", "found": True, "_source": {"title": "D"}}, + ] + }, + ] + usage_client = Mock(client=client) + platform = Mock(acron3="scl") + + records = list(ReportQuery(usage_client).fetch(platform, [date(2024, 1, 1)])) + + self.assertEqual(len(records), 2) + self.assertEqual({source["title"] for _, source, _ in records}, {"A", "B"}) + self.assertEqual({document["title"] for _, _, document in records}, {"C", "D"}) + self.assertEqual(client.search.call_count, 2) + self.assertEqual(client.mget.call_count, 4) From 848339ed483f9ca58a7d4d1cdd8050db80ebb963 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:39 -0300 Subject: [PATCH 20/47] Inclui o esquema oficial da COUNTER Release 5.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mantém o esquema somente como apoio da validação automatizada dos relatórios gerados nos testes. --- counter_api/schemas/COUNTER_API_5_1_1.json | 18061 +++++++++++++++++++ counter_api/tests/schema.py | 26 + 2 files changed, 18087 insertions(+) create mode 100644 counter_api/schemas/COUNTER_API_5_1_1.json create mode 100644 counter_api/tests/schema.py diff --git a/counter_api/schemas/COUNTER_API_5_1_1.json b/counter_api/schemas/COUNTER_API_5_1_1.json new file mode 100644 index 00000000..76dc79dc --- /dev/null +++ b/counter_api/schemas/COUNTER_API_5_1_1.json @@ -0,0 +1,18061 @@ +{ + "openapi": "3.1.0", + "x-stoplight": { + "id": "au9uaf0yg84mo" + }, + "info": { + "title": "COUNTER API", + "version": "5.1.1", + "description": "This version of the COUNTER API (formerly sushi) applies to Release 5.1.1 of the COUNTER Code of Practice, which can be read online at https://cop5.countermetrics.org/.\n\nThis workspace provides a detailed description of the entire COUNTER API, including the JSON schema for COUNTER Reports and Standard Views of the COUNTER Reports. \n\nIt is expected that reporting services will use only the parts relevant to them, or make local tailored copies relevant to their particular circumstances, for example by removing methods detailing reports they do not support.\n\nIf you have any queries, please contact the Executive Director (code@countermetrics.org).", + "summary": "COUNTER API for Release 5.1.1", + "license": { + "name": "CC BY 4.0", + "url": "https://creativecommons.org/licenses/by/4.0/" + }, + "contact": { + "name": "COUNTER Executive Director", + "email": "code@countermetrics.org" + } + }, + "tags": [ + { + "name": "Server Status", + "description": "Path and models specific for Server Status" + }, + { + "name": "Member List", + "description": "Path and models specific for Member List" + }, + { + "name": "Platform List", + "description": "Path and models specific for Platform List" + }, + { + "name": "Report List", + "description": "Path and models specific for Report List" + }, + { + "name": "PR - Platform Report", + "description": "Path and models specific for the COUNTER Report TR" + }, + { + "name": "DR - Database Report", + "description": "Path and models specific for the COUNTER Report DR" + }, + { + "name": "TR - Title Report", + "description": "Path and models specific for the COUNTER Report TR" + }, + { + "name": "IR - Item Report", + "description": "Path and models specific for the COUNTER Report IR" + }, + { + "name": "PR_P1 - Platform Usage", + "description": "Path and models specific for the Standard View PR_P1" + }, + { + "name": "DR_D1 - Database Search and Item Usage", + "description": "Path and models specific for the Standard View DR_D1" + }, + { + "name": "DR_D2 - Database Access Denied", + "description": "Path and models specific for the Standard View DR_D2" + }, + { + "name": "TR_B1 - Book Requests (Controlled)", + "description": "Path and models specific for the Standard View TR_B1" + }, + { + "name": "TR_B2 - Book Access Denied", + "description": "Path and models specific for the Standard View TR_B2" + }, + { + "name": "TR_B3 - Book Usage by Access Type", + "description": "Path and models specific for the Standard View TR_B3" + }, + { + "name": "TR_J1 - Journal Requests (Controlled)", + "description": "Path and models specific for the Standard View TR_J1" + }, + { + "name": "TR_J2 - Journal Access Denied", + "description": "Path and models specific for the Standard View TR_J2" + }, + { + "name": "TR_J3 - Journal Usage by Access Type", + "description": "Path and models specific for the Standard View TR_J3" + }, + { + "name": "TR_J4 - Journal Requests by YOP (Controlled)", + "description": "Path and models specific for the Standard View TR_J4" + }, + { + "name": "IR_A1 - Journal Article Requests", + "description": "Path and models specific for the Standard View IR_A1" + }, + { + "name": "IR_M1 - Multimedia Item Requests", + "description": "Path and models specific for the Standard View IR_M1" + }, + { + "name": "Exceptions", + "description": "Exceptions models" + }, + { + "name": "Shared Models", + "description": "Models used by multiple paths and path specific models" + } + ], + "paths": { + "/r51/status": { + "parameters": [], + "get": { + "summary": "Server Status", + "operationId": "get-r51-status", + "description": "Returns the current status of the reporting service(s) supported by the COUNTER API server.\n\nThis path MUST be public to allow report consumers to easily check whether a COUNTER API server is live.", + "parameters": [ + { + "$ref": "#/components/parameters/platform" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_Status" + } + }, + "tags": [ + "Server Status" + ], + "x-stoplight": { + "id": "8bmbushs5pxel" + } + } + }, + "/r51/members": { + "get": { + "summary": "Member List", + "tags": [ + "Member List" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_Members" + }, + "400": { + "$ref": "#/components/responses/400_Exception_1030" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-members", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + } + ], + "description": "Returns the institutions associated with the customer ID in the request and their customer IDs and requestor IDs (if required for API calls and differing from the requestor ID in the request).\n\nThe associated institutions can be consortium members, or sites for multi-site customers. If the customer in the request is neither a consortium nor multi-site, the list only includes the details for the customer itself.\n\nThis path is a good choice for testing COUNTER API credentials.", + "x-stoplight": { + "id": "cpeuj2etekukw" + } + }, + "parameters": [] + }, + "/r51/platforms": { + "get": { + "summary": "Platform List", + "tags": [ + "Platform List" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_Platforms" + }, + "400": { + "$ref": "#/components/responses/400_Exception_1030" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-platforms", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + } + ], + "description": "Returns the list of platforms supported by the COUNTER API server for the credentials included in the request. The response SHOULD be customer specific if possible.\n\nThis API path is a common extension (see Section 11.7 of the Code of Practice for details). Support for common extensions is OPTIONAL, i.e. report providers can decide whether to support this API path or not.", + "x-stoplight": { + "id": "f3klmp9xqr2wd" + } + }, + "parameters": [] + }, + "/r51/reports": { + "get": { + "summary": "Report List", + "tags": [ + "Report List" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_Reports" + }, + "400": { + "$ref": "#/components/responses/400_Exception_1030" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/include_month_details" + } + ], + "description": "Returns the list of reports supported by the COUNTER API server for the customer, including the first and last months for which usage data has been processed and is available. This information SHOULD be specific for the customer ID.", + "x-stoplight": { + "id": "ntn2mj2pv2b9g" + } + }, + "parameters": [] + }, + "/r51/reports/pr": { + "get": { + "summary": "PR - Platform Report", + "tags": [ + "PR - Platform Report" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_PR" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-pr", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + }, + { + "$ref": "#/components/parameters/metric_type" + }, + { + "$ref": "#/components/parameters/data_type" + }, + { + "$ref": "#/components/parameters/access_method" + }, + { + "$ref": "#/components/parameters/attributes_to_show" + }, + { + "$ref": "#/components/parameters/granularity" + }, + { + "$ref": "#/components/parameters/attributed" + }, + { + "$ref": "#/components/parameters/country_code" + }, + { + "$ref": "#/components/parameters/subdivision_code" + } + ], + "description": "Returns the COUNTER Report PR - Platform Report.\n\nPR is a customizable report summarizing activity across a report provider’s platforms that allows the user to apply filters and select other configuration options.", + "x-stoplight": { + "id": "wil5vg2nw7875" + } + }, + "parameters": [] + }, + "/r51/reports/pr_p1": { + "get": { + "summary": "PR_P1 - Platform Usage", + "tags": [ + "PR_P1 - Platform Usage" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_PR_P1" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-pr_p1", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the PR_P1 Standard Views of the Platform Report - Platform Usage.\n\nPR_P1 reports on platform-level usage summarized by Metric_Type.", + "x-stoplight": { + "id": "14woct8zgfn7y" + } + }, + "parameters": [] + }, + "/r51/reports/dr": { + "get": { + "summary": "DR - Database Report", + "tags": [ + "DR - Database Report" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_DR" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-dr", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + }, + { + "$ref": "#/components/parameters/metric_type" + }, + { + "$ref": "#/components/parameters/data_type" + }, + { + "$ref": "#/components/parameters/access_method" + }, + { + "$ref": "#/components/parameters/attributes_to_show" + }, + { + "$ref": "#/components/parameters/granularity" + }, + { + "$ref": "#/components/parameters/database" + }, + { + "$ref": "#/components/parameters/attributed" + }, + { + "$ref": "#/components/parameters/country_code" + }, + { + "$ref": "#/components/parameters/subdivision_code" + } + ], + "description": "Returns the COUNTER Report DR - Database Report.\n\nDR is a customizable report detailing activity by database that allows the user to apply filters and select other configuration options.", + "x-stoplight": { + "id": "182f2d81pwsnl" + } + }, + "parameters": [] + }, + "/r51/reports/dr_d1": { + "get": { + "summary": "DR_D1 - Database Search and Item Usage", + "tags": [ + "DR_D1 - Database Search and Item Usage" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_DR_D1" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-dr_d1", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the DR_D1 Standard View of the Database Report - Database Search and Item Usage.\n\nDR_D1 reports on key Searches, Investigations and Requests metrics needed to evaluate a database.", + "x-stoplight": { + "id": "z8bik3hyq48vh" + } + }, + "parameters": [] + }, + "/r51/reports/dr_d2": { + "get": { + "summary": "DR_D2 - Database Access Denied", + "tags": [ + "DR_D2 - Database Access Denied" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_DR_D2" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-dr_d2", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the DR_D2 Standard View of the Database Report - Database Access Denied.\n\nDR_D2 reports on Access Denied activity for databases where users were denied access because simultaneous-use licenses were exceeded or their institution did not have a license for the database.", + "x-stoplight": { + "id": "z7nirme3tiy89" + } + }, + "parameters": [] + }, + "/r51/reports/tr": { + "get": { + "summary": "TR - Title Report", + "tags": [ + "TR - Title Report" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_TR" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-tr", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + }, + { + "$ref": "#/components/parameters/metric_type" + }, + { + "$ref": "#/components/parameters/data_type" + }, + { + "$ref": "#/components/parameters/access_method" + }, + { + "$ref": "#/components/parameters/access_type" + }, + { + "$ref": "#/components/parameters/yop" + }, + { + "$ref": "#/components/parameters/attributes_to_show" + }, + { + "$ref": "#/components/parameters/granularity" + }, + { + "$ref": "#/components/parameters/item_id" + }, + { + "$ref": "#/components/parameters/attributed" + }, + { + "$ref": "#/components/parameters/country_code" + }, + { + "$ref": "#/components/parameters/subdivision_code" + } + ], + "description": "Returns the COUNTER Report TR - Title Report.\n\nTR is a customizable report detailing activity at the title level (journal, book, etc.) that allows the user to apply filters and select other configuration options.", + "x-stoplight": { + "id": "ggc46up3hl3qn" + } + }, + "parameters": [] + }, + "/r51/reports/tr_b1": { + "get": { + "summary": "TR_B1 - Book Requests (Controlled)", + "tags": [ + "TR_B1 - Book Requests (Controlled)" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_TR_B1" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-tr_b1", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the TR_B1 Standard View of the Title Report - Book Requests (Controlled).\n\nTR_B1 reports on full-text activity for books (Data_Types Book and Reference_Work), excluding Open and Free_To_Read content, as Total_Item_Requests and Unique_Title_Requests. The Unique_Title_Requests provides comparable usage across book platforms. The Total_Item_Requests shows overall activity; however, numbers between sites will vary significantly based on how the content is delivered (e.g. delivered as a complete book or by chapter).", + "x-stoplight": { + "id": "b7a02ayvg1t7o" + } + }, + "parameters": [] + }, + "/r51/reports/tr_b2": { + "get": { + "summary": "TR_B2 - Book Access Denied", + "tags": [ + "TR_B2 - Book Access Denied" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_TR_B2" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-tr_b2", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the TR_B2 Standard View of the Title Report - Book Access Denied.\n\nTR_B2 reports on Access Denied activity for books (Data_Types Book and Reference_Work) where users were denied access because simultaneous-use licenses were exceeded or their institution did not have a license for the book.", + "x-stoplight": { + "id": "q94cza8f3zvsl" + } + }, + "parameters": [] + }, + "/r51/reports/tr_b3": { + "get": { + "summary": "TR_B3 - Book Usage by Access Type", + "tags": [ + "TR_B3 - Book Usage by Access Type" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_TR_B3" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-tr_b3", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the TR_B3 Standard View of the Title Report - Book Usage by Access Type.\n\nTR_B3 reports on book usage (Data_Types Book and Reference_Work) showing all applicable Metric_Types broken down by Access_Type.", + "x-stoplight": { + "id": "e0zfomsyrw4fw" + } + }, + "parameters": [] + }, + "/r51/reports/tr_j1": { + "get": { + "summary": "TR_J1 - Journal Requests (Controlled)", + "tags": [ + "TR_J1 - Journal Requests (Controlled)" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_TR_J1" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-tr_j1", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the TR_J1 Standard View of the Title Report - Journal Requests (Controlled).\n\nTR_J1 reports on usage of journal content, excluding Open and Free_To_Read content, as Total_Item_Requests and Unique_Item_Requests. The Unique_Item_Requests provides comparable usage across journal platforms by reducing the inflationary effect that occurs when an HTML full text automatically displays and the user then accesses the PDF version. The Total_Item_Requests shows overall activity.", + "x-stoplight": { + "id": "tgmjh03kpps4d" + } + }, + "parameters": [] + }, + "/r51/reports/tr_j2": { + "get": { + "summary": "TR_J2 - Journal Access Denied", + "tags": [ + "TR_J2 - Journal Access Denied" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_TR_J2" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-tr_j2", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the TR_J2 Standard View of the Title Report - Journal Access Denied.\n\nTR_J2 reports on Access Denied activity for journal content where users were denied access because simultaneous-use licenses were exceeded or their institution did not have a license for the title.", + "x-stoplight": { + "id": "42tb3n2z00efu" + } + }, + "parameters": [] + }, + "/r51/reports/tr_j3": { + "get": { + "summary": "TR_J3 - Journal Usage by Access Type", + "tags": [ + "TR_J3 - Journal Usage by Access Type" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_TR_J3" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-tr_j3", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the TR_J3 Standard View of the Title Report - Journal Usage by Access Type.\n\nTR_J3 reports on usage of journal content for all Metric_Types broken down by Access_Type.", + "x-stoplight": { + "id": "5tg1fkbs8ttfa" + } + }, + "parameters": [] + }, + "/r51/reports/tr_j4": { + "get": { + "summary": "TR_J4 - Journal Requests by YOP (Controlled)", + "tags": [ + "TR_J4 - Journal Requests by YOP (Controlled)" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_TR_J4" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-tr_j4", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the TR_J4 Standard View of the Title Report- Journal Requests by YOP (Controlled).\n\nTR_J4 breaks down the usage of journal content, excluding Open and Free_To_Read content, by year of publication (YOP), providing counts for the Metric_Types Total_Item_Requests and Unique_Item_Requests. Provides the details necessary to analyze usage of content in backfiles or covered by perpetual access agreement. Note that COUNTER reports do not provide access model or perpetual access rights details.", + "x-stoplight": { + "id": "if2iaic3b29f0" + } + }, + "parameters": [] + }, + "/r51/reports/ir": { + "get": { + "summary": "IR - Item Report", + "tags": [ + "IR - Item Report" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_IR" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-ir", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + }, + { + "$ref": "#/components/parameters/metric_type" + }, + { + "$ref": "#/components/parameters/data_type" + }, + { + "$ref": "#/components/parameters/access_method" + }, + { + "$ref": "#/components/parameters/access_type" + }, + { + "$ref": "#/components/parameters/yop" + }, + { + "$ref": "#/components/parameters/attributes_to_show" + }, + { + "$ref": "#/components/parameters/granularity" + }, + { + "$ref": "#/components/parameters/include_parent_details" + }, + { + "$ref": "#/components/parameters/include_component_details" + }, + { + "$ref": "#/components/parameters/item_id" + }, + { + "$ref": "#/components/parameters/author" + }, + { + "$ref": "#/components/parameters/attributed" + }, + { + "$ref": "#/components/parameters/country_code" + }, + { + "$ref": "#/components/parameters/subdivision_code" + } + ], + "description": "Returns the COUNTER Report IR - Item Report.\n\nIR is a granular, customizable report showing activity at the level of the item (article, chapter, media object, etc.) that allows the user to apply filters and select other configuration options.", + "x-stoplight": { + "id": "r22dp0fotaqts" + } + }, + "parameters": [] + }, + "/r51/reports/ir_a1": { + "get": { + "summary": "IR_A1 - Journal Article Requests", + "tags": [ + "IR_A1 - Journal Article Requests" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_IR_A1" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-ir_a1", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the IR_A1 Standard View of the Item Report - Journal Article Requests.\n\nIR_A1 reports on journal article requests at the article level. This report is limited to content with a Data_Type of Article and Metric_Types of Total_Item_Requests and Unique_Item_Requests.\n\n", + "x-stoplight": { + "id": "f2uhz9w9zwnha" + } + }, + "parameters": [] + }, + "/r51/reports/ir_m1": { + "get": { + "summary": "IR_M1 - Multimedia Item Requests", + "tags": [ + "IR_M1 - Multimedia Item Requests" + ], + "responses": { + "200": { + "$ref": "#/components/responses/200_IR_M1" + }, + "202": { + "$ref": "#/components/responses/202_Exception" + }, + "400": { + "$ref": "#/components/responses/400_Exception" + }, + "401": { + "$ref": "#/components/responses/401_Exception" + }, + "403": { + "$ref": "#/components/responses/403_Exception" + }, + "429": { + "$ref": "#/components/responses/429_Exception" + }, + "503": { + "$ref": "#/components/responses/503_Exception" + } + }, + "operationId": "get-r51-reports-ir_m1", + "parameters": [ + { + "$ref": "#/components/parameters/customer_id" + }, + { + "$ref": "#/components/parameters/requestor_id" + }, + { + "$ref": "#/components/parameters/api_key" + }, + { + "$ref": "#/components/parameters/platform" + }, + { + "$ref": "#/components/parameters/begin_date" + }, + { + "$ref": "#/components/parameters/end_date" + } + ], + "description": "Returns the IR_M1 Standard View of the Item Report - Multimedia Item Requests.\n\nIR_M1 reports on multimedia requests at the item level.", + "x-stoplight": { + "id": "bb8z0h7dptt9i" + } + }, + "parameters": [] + } + }, + "components": { + "schemas": { + "Status": { + "x-stoplight": { + "id": "ar3g2lwa3lo37" + }, + "type": "object", + "properties": { + "Description": { + "type": "string", + "description": "Description of the service." + }, + "Service_Active": { + "type": "boolean", + "description": "Indicator if the service is currently able to deliver reports." + }, + "Registry_Record": { + "type": "string", + "pattern": "^(https://registry.countermetrics.org/(platform|usage-data-host)/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|)$", + "description": "The link to the platform’s COUNTER Registry record. Report providers who do not have a Registry record MUST leave the value blank." + }, + "Note": { + "type": "string", + "description": "A general note about the service." + }, + "Alerts": { + "type": "array", + "description": "Any alerts related to service interruptions and status.", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "Date_Time": { + "type": "string", + "description": "Date-time of the alert, in RFC3339 date-time format.", + "format": "date-time" + }, + "Alert": { + "type": "string", + "description": "Text of the alert." + } + } + } + } + }, + "required": [ + "Service_Active", + "Registry_Record" + ], + "examples": [ + { + "Description": "COUNTER Usage Reports for EBSCOhost platform.", + "Service_Active": true, + "Registry_Record": "https://registry.countermetrics.org/platform/b2b2736c-2cb9-48ec-91f4-870336acfb1c", + "Alerts": [ + { + "Date_Time": "2016-08-02T12:54:05Z", + "Alert": "Service will be unavailable Sunday midnight." + } + ] + } + ], + "description": "Status of a reporting service supported by a COUNTER API server.", + "title": "Status Information", + "x-tags": [ + "Server Status" + ] + }, + "Member": { + "x-stoplight": { + "id": "cb2lt5eknvxn6" + }, + "title": "Member Information", + "description": "Customer ID and (if required for API requests) requestor ID for an institution.", + "type": "object", + "additionalProperties": false, + "properties": { + "Institution_Name": { + "type": "string", + "minLength": 2, + "description": "Name of the institution." + }, + "Institution_ID": { + "$ref": "#/components/schemas/Institution_ID", + "description": "Other identifiers for the institution (besides the customer ID)." + }, + "Customer_ID": { + "type": "string", + "minLength": 1, + "description": "Customer ID for requests to the API for this institution." + }, + "Requestor_ID": { + "type": "string", + "minLength": 1, + "description": "Requestor ID for requests to the API for the institution. This element MUST be omitted if the requestor ID is identical to the requestor ID in the request (or the COUNTER API server does not use requestor IDs)." + }, + "Notes": { + "type": "string", + "minLength": 2, + "description": "Additional notes regarding the institution." + } + }, + "required": [ + "Institution_Name", + "Customer_ID" + ], + "examples": [ + { + "Customer_ID": "C12345", + "Institution_Name": "Sample Institution", + "Notes": "This is a sample customer", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + } + } + ], + "x-tags": [ + "Member List" + ] + }, + "Platform": { + "type": "object", + "title": "Platform Information", + "description": "Information about a platform supported by the COUNTER API server.", + "x-stoplight": { + "id": "tz8njvy4bcqh1" + }, + "examples": [ + { + "Platform_Parameter": "12", + "Platform_Name": "EBSCOhost" + }, + { + "Platform_Parameter": "23", + "Platform_Name": "ProQuest", + "Platform_Description": "Reports for the www.proquest.com platform" + }, + { + "Platform_Parameter": "41", + "Platform_Name": "ScienceDirect" + } + ], + "x-tags": [ + "Platform List" + ], + "required": [ + "Platform_Parameter", + "Platform_Name" + ], + "additionalProperties": false, + "properties": { + "Platform_Parameter": { + "type": "string", + "minLength": 1, + "description": "Value to be used for the platform parameter on other API paths." + }, + "Platform_Name": { + "type": "string", + "minLength": 1, + "description": "Name of the platform." + }, + "Platform_Description": { + "type": "string", + "minLength": 1, + "description": "Additional information about the platform." + } + } + }, + "Report": { + "type": "object", + "examples": [ + { + "Report_Name": "Platform Report", + "Report_ID": "PR", + "Release": "5.1", + "Report_Description": "A customizable report summarizing activity across a report provider’s platforms that allows the user to apply filters and select other configuration options.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Platform Usage", + "Report_ID": "PR_P1", + "Release": "5.1", + "Report_Description": "Platform-level usage summarized by Metric_Type.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Database Report", + "Report_ID": "DR", + "Release": "5.1", + "Report_Description": "A customizable report detailing activity by database that allows the user to apply filters and select other configuration options.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Database Search and Item Usage", + "Report_ID": "DR_D1", + "Release": "5.1", + "Report_Description": "Reports on key Searches, Investigations and Requests metrics needed to evaluate a database.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Database Access Denied", + "Report_ID": "DR_D2", + "Release": "5.1", + "Report_Description": "Reports on Access Denied activity for databases where users were denied access because simultaneous-use licenses were exceeded or their institution did not have a license for the database.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Title Report", + "Report_ID": "TR", + "Release": "5.1", + "Report_Description": "A customizable report detailing activity at the title level (journal, book, etc.) that allows the user to apply filters and select other configuration options.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Book Requests (Controlled)", + "Report_ID": "TR_B1", + "Release": "5.1", + "Report_Description": "Reports on full-text activity for books, excluding Open and Free_To_Read content, as Total_Item_Requests and Unique_Title_Requests.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Book Access Denied", + "Report_ID": "TR_B2", + "Release": "5.1", + "Report_Description": "Reports on Access Denied activity for books where users were denied access because simultaneous-use licenses were exceeded or their institution did not have a license for the book.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Book Usage by Access Type", + "Report_ID": "TR_B3", + "Release": "5.1", + "Report_Description": "Reports on book usage showing all applicable Metric_Types broken down by Access_Type.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Journal Requests (Controlled)", + "Report_ID": "TR_J1", + "Release": "5.1", + "Report_Description": "Reports on usage of journal content, excluding Open and Free_To_Read content, as Total_Item_Requests and Unique_Item_Requests.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Journal Access Denied", + "Report_ID": "TR_J2", + "Release": "5.1", + "Report_Description": "Reports on Access Denied activity for journal content where users were denied access because simultaneous-use licenses were exceeded or their institution did not have a license for the title.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Journal Usage by Access Type", + "Report_ID": "TR_J3", + "Release": "5.1", + "Report_Description": "Reports on usage of journal content for all Metric_Types broken down by Access_Type.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Journal Requests by YOP (Controlled)", + "Report_ID": "TR_J4", + "Release": "5.1", + "Report_Description": "Breaks down the usage of journal content, excluding Open and Free_To_Read content, by year of publication (YOP), providing counts for the Metric_Types Total_Item_Requests and Unique_Item_Requests.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Item Report", + "Report_ID": "IR", + "Release": "5.1", + "Report_Description": "A granular, customizable report showing activity at the level of the item (article, chapter, media object, etc.) that allows the user to apply filters and select other configuration options.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Journal Article Requests", + "Report_ID": "IR_A1", + "Release": "5.1", + "Report_Description": "Reports on journal article requests at the article level. This report is limited to content with a Data_Type of Article and Metric_Types of Total_Item_Requests and Unique_Item_Requests.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Multimedia Item Requests", + "Report_ID": "IR_M1", + "Release": "5.1", + "Report_Description": "Reports on multimedia requests at the item level.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + } + ], + "title": "Report Information", + "description": "Information about a report supported by a COUNTER API server.", + "x-stoplight": { + "id": "bntdmeog8lmwo" + }, + "properties": { + "Report_Name": { + "type": "string", + "minLength": 2, + "description": "Name of the report. For COUNTER Reports and Stadard Views of COUNTER Reports this is the Report_Name in the Report_Header." + }, + "Report_ID": { + "type": "string", + "minLength": 2, + "description": "ID of the report used in the request (in lower case). For COUNTER Reports and Standard Views of COUNTER Reports this is the Report_ID in the Report_Header." + }, + "Release": { + "type": "string", + "const": "5.1", + "description": "Release version of the report." + }, + "Report_Description": { + "type": "string", + "minLength": 2, + "description": "Short description of the report." + }, + "Path": { + "type": "string", + "minLength": 2, + "description": "Path to use when requesting this report. Optional but recommended for custom reports. For COUNTER Reports and Standard Views of COUNTER Reports the path MUST be identical with the path specified in the Code of Practice, if the element is not omitted." + }, + "First_Month_Available": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}$", + "description": "First month for which usage data has been processed and is available." + }, + "Last_Month_Available": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}$", + "description": "Last month for which usage data has been processed and is available." + }, + "Month_Details": { + "$ref": "#/components/schemas/Month_Details" + } + }, + "required": [ + "Report_Name", + "Report_ID", + "Release", + "Report_Description", + "First_Month_Available", + "Last_Month_Available" + ], + "x-tags": [ + "Report List" + ] + }, + "Month_Details": { + "x-stoplight": { + "id": "eg0awspy261s0" + }, + "title": "Month_Details", + "description": "Information about when (and why) data for the report has changed. This is a common extension, support is OPTIONAL (see Section 11.7 of the Code of Practice for details).", + "type": "object", + "patternProperties": { + "^[0-9]{4}-(0[1-9]|1[0-2])$": { + "type": "object", + "description": "Month for which the data has changed, in yyyy-mm format.", + "properties": { + "Last_Change_Date": { + "type": "string", + "description": "Date-time when the data for the month last changed, in RFC3339 date-time format.", + "format": "date-time" + }, + "Last_Change_Note": { + "type": "string", + "description": "Information about why the data for the month has changed." + } + }, + "required": [ + "Last_Change_Date" + ] + } + }, + "minProperties": 1, + "additionalProperties": false, + "examples": [ + { + "2026-03": { + "Last_Change_Date": "2026-04-18T20:30:40Z", + "Last_Change_Note": "Data restated after correction of duplicate request counts." + }, + "2026-02": { + "Last_Change_Date": "2026-03-29T01:02:03Z" + } + } + ], + "x-tags": [ + "Shared Models" + ] + }, + "PR": { + "title": "PR - Platform Report", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/PR_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PR_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "06fikws8lv1ls" + }, + "x-tags": [ + "PR - Platform Report" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "PR", + "Report_Name": "Platform Report", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Attributes": { + "Attributes_To_Show": [ + "Access_Method" + ] + }, + "Report_Filters": { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + }, + "Report_Items": [ + { + "Platform": "Platform 1", + "Attribute_Performance": [ + { + "Data_Type": "Article", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1461, + "2022-02": 1704, + "2022-03": 1475 + }, + "Total_Item_Requests": { + "2022-01": 877, + "2022-02": 1022, + "2022-03": 885 + }, + "Unique_Item_Investigations": { + "2022-01": 1096, + "2022-02": 1278, + "2022-03": 1106 + }, + "Unique_Item_Requests": { + "2022-01": 658, + "2022-02": 767, + "2022-03": 664 + } + } + }, + { + "Data_Type": "Audiovisual", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1279, + "2022-02": 1323, + "2022-03": 1826 + }, + "Total_Item_Requests": { + "2022-01": 767, + "2022-02": 794, + "2022-03": 1096 + }, + "Unique_Item_Investigations": { + "2022-01": 959, + "2022-02": 992, + "2022-03": 1370 + }, + "Unique_Item_Requests": { + "2022-01": 575, + "2022-02": 596, + "2022-03": 822 + } + } + }, + { + "Data_Type": "Book", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + }, + { + "Data_Type": "Book_Segment", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1156, + "2022-02": 1289, + "2022-03": 1821 + }, + "Total_Item_Requests": { + "2022-01": 694, + "2022-02": 773, + "2022-03": 1093 + }, + "Unique_Item_Investigations": { + "2022-01": 867, + "2022-02": 967, + "2022-03": 1366 + }, + "Unique_Item_Requests": { + "2022-01": 521, + "2022-02": 580, + "2022-03": 820 + } + } + }, + { + "Data_Type": "Conference", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1253, + "2022-02": 1070, + "2022-03": 1319 + }, + "Total_Item_Requests": { + "2022-01": 752, + "2022-02": 642, + "2022-03": 791 + }, + "Unique_Item_Investigations": { + "2022-01": 940, + "2022-02": 803, + "2022-03": 989 + }, + "Unique_Item_Requests": { + "2022-01": 564, + "2022-02": 482, + "2022-03": 593 + } + } + }, + { + "Data_Type": "Conference_Item", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1877, + "2022-02": 1261, + "2022-03": 1335 + }, + "Total_Item_Requests": { + "2022-01": 1126, + "2022-02": 757, + "2022-03": 801 + }, + "Unique_Item_Investigations": { + "2022-01": 1408, + "2022-02": 946, + "2022-03": 1001 + }, + "Unique_Item_Requests": { + "2022-01": 845, + "2022-02": 568, + "2022-03": 601 + } + } + }, + { + "Data_Type": "Database_Full_Item", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1094, + "2022-02": 1325, + "2022-03": 1730 + }, + "Total_Item_Requests": { + "2022-01": 656, + "2022-02": 795, + "2022-03": 1038 + }, + "Unique_Item_Investigations": { + "2022-01": 821, + "2022-02": 994, + "2022-03": 1298 + }, + "Unique_Item_Requests": { + "2022-01": 492, + "2022-02": 596, + "2022-03": 779 + } + } + }, + { + "Data_Type": "Dataset", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1329, + "2022-02": 1501, + "2022-03": 1276 + }, + "Total_Item_Requests": { + "2022-01": 797, + "2022-02": 901, + "2022-03": 766 + }, + "Unique_Item_Investigations": { + "2022-01": 997, + "2022-02": 1126, + "2022-03": 957 + }, + "Unique_Item_Requests": { + "2022-01": 598, + "2022-02": 676, + "2022-03": 575 + } + } + }, + { + "Data_Type": "Image", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1732, + "2022-02": 1194, + "2022-03": 1867 + }, + "Total_Item_Requests": { + "2022-01": 1039, + "2022-02": 716, + "2022-03": 1120 + }, + "Unique_Item_Investigations": { + "2022-01": 1299, + "2022-02": 896, + "2022-03": 1400 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 537, + "2022-03": 840 + } + } + }, + { + "Data_Type": "Interactive_Resource", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1769, + "2022-02": 1947, + "2022-03": 1060 + }, + "Total_Item_Requests": { + "2022-01": 1061, + "2022-02": 1168, + "2022-03": 636 + }, + "Unique_Item_Investigations": { + "2022-01": 1327, + "2022-02": 1460, + "2022-03": 795 + }, + "Unique_Item_Requests": { + "2022-01": 796, + "2022-02": 876, + "2022-03": 477 + } + } + }, + { + "Data_Type": "Journal", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1002, + "2022-02": 1650, + "2022-03": 1623 + }, + "Total_Item_Requests": { + "2022-01": 601, + "2022-02": 990, + "2022-03": 974 + }, + "Unique_Item_Investigations": { + "2022-01": 752, + "2022-02": 1238, + "2022-03": 1217 + }, + "Unique_Item_Requests": { + "2022-01": 451, + "2022-02": 743, + "2022-03": 731 + } + } + }, + { + "Data_Type": "Multimedia", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1865, + "2022-02": 1891, + "2022-03": 1707 + }, + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Investigations": { + "2022-01": 1399, + "2022-02": 1418, + "2022-03": 1280 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + }, + { + "Data_Type": "News_Item", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1687, + "2022-02": 1732, + "2022-03": 1025 + }, + "Total_Item_Requests": { + "2022-01": 1012, + "2022-02": 1039, + "2022-03": 615 + }, + "Unique_Item_Investigations": { + "2022-01": 1265, + "2022-02": 1299, + "2022-03": 769 + }, + "Unique_Item_Requests": { + "2022-01": 759, + "2022-02": 779, + "2022-03": 461 + } + } + }, + { + "Data_Type": "Newspaper_or_Newsletter", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1773, + "2022-02": 1527, + "2022-03": 1366 + }, + "Total_Item_Requests": { + "2022-01": 1064, + "2022-02": 916, + "2022-03": 820 + }, + "Unique_Item_Investigations": { + "2022-01": 1330, + "2022-02": 1145, + "2022-03": 1025 + }, + "Unique_Item_Requests": { + "2022-01": 798, + "2022-02": 687, + "2022-03": 615 + } + } + }, + { + "Data_Type": "Other", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1730, + "2022-02": 1263, + "2022-03": 1787 + }, + "Total_Item_Requests": { + "2022-01": 1038, + "2022-02": 758, + "2022-03": 1072 + }, + "Unique_Item_Investigations": { + "2022-01": 1298, + "2022-02": 947, + "2022-03": 1340 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 569, + "2022-03": 804 + } + } + }, + { + "Data_Type": "Patent", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1012, + "2022-02": 1074, + "2022-03": 1259 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 644, + "2022-03": 755 + }, + "Unique_Item_Investigations": { + "2022-01": 759, + "2022-02": 806, + "2022-03": 944 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 483, + "2022-03": 566 + } + } + }, + { + "Data_Type": "Platform", + "Access_Method": "Regular", + "Performance": { + "Searches_Platform": { + "2022-01": 17484, + "2022-02": 18061, + "2022-03": 17160 + } + } + }, + { + "Data_Type": "Reference_Item", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1546, + "2022-02": 1408, + "2022-03": 1204 + }, + "Total_Item_Requests": { + "2022-01": 928, + "2022-02": 845, + "2022-03": 722 + }, + "Unique_Item_Investigations": { + "2022-01": 1160, + "2022-02": 1056, + "2022-03": 903 + }, + "Unique_Item_Requests": { + "2022-01": 696, + "2022-02": 634, + "2022-03": 542 + } + } + }, + { + "Data_Type": "Reference_Work", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1645, + "2022-02": 1022, + "2022-03": 1106 + }, + "Total_Item_Requests": { + "2022-01": 987, + "2022-02": 613, + "2022-03": 664 + }, + "Unique_Item_Investigations": { + "2022-01": 1234, + "2022-02": 767, + "2022-03": 830 + }, + "Unique_Item_Requests": { + "2022-01": 740, + "2022-02": 460, + "2022-03": 498 + }, + "Unique_Title_Investigations": { + "2022-01": 301, + "2022-02": 283, + "2022-03": 217 + }, + "Unique_Title_Requests": { + "2022-01": 226, + "2022-02": 212, + "2022-03": 163 + } + } + }, + { + "Data_Type": "Report", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1381, + "2022-02": 1852, + "2022-03": 1726 + }, + "Total_Item_Requests": { + "2022-01": 829, + "2022-02": 1111, + "2022-03": 1036 + }, + "Unique_Item_Investigations": { + "2022-01": 1036, + "2022-02": 1389, + "2022-03": 1295 + }, + "Unique_Item_Requests": { + "2022-01": 622, + "2022-02": 833, + "2022-03": 777 + } + } + }, + { + "Data_Type": "Software", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1187, + "2022-02": 1775, + "2022-03": 1416 + }, + "Total_Item_Requests": { + "2022-01": 712, + "2022-02": 1065, + "2022-03": 850 + }, + "Unique_Item_Investigations": { + "2022-01": 890, + "2022-02": 1331, + "2022-03": 1062 + }, + "Unique_Item_Requests": { + "2022-01": 534, + "2022-02": 799, + "2022-03": 638 + } + } + }, + { + "Data_Type": "Sound", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1460, + "2022-02": 1905, + "2022-03": 1777 + }, + "Total_Item_Requests": { + "2022-01": 876, + "2022-02": 1143, + "2022-03": 1066 + }, + "Unique_Item_Investigations": { + "2022-01": 1095, + "2022-02": 1429, + "2022-03": 1333 + }, + "Unique_Item_Requests": { + "2022-01": 657, + "2022-02": 857, + "2022-03": 800 + } + } + }, + { + "Data_Type": "Standard", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1800, + "2022-02": 1721, + "2022-03": 1251 + }, + "Total_Item_Requests": { + "2022-01": 1080, + "2022-02": 1033, + "2022-03": 751 + }, + "Unique_Item_Investigations": { + "2022-01": 1350, + "2022-02": 1291, + "2022-03": 938 + }, + "Unique_Item_Requests": { + "2022-01": 810, + "2022-02": 775, + "2022-03": 563 + } + } + }, + { + "Data_Type": "Thesis_or_Dissertation", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1970, + "2022-02": 1930, + "2022-03": 1981 + }, + "Total_Item_Requests": { + "2022-01": 1182, + "2022-02": 1158, + "2022-03": 1189 + }, + "Unique_Item_Investigations": { + "2022-01": 1478, + "2022-02": 1448, + "2022-03": 1486 + }, + "Unique_Item_Requests": { + "2022-01": 887, + "2022-02": 869, + "2022-03": 892 + } + } + }, + { + "Data_Type": "Unspecified", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1011, + "2022-02": 1828, + "2022-03": 1830 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 1097, + "2022-03": 1098 + }, + "Unique_Item_Investigations": { + "2022-01": 758, + "2022-02": 1371, + "2022-03": 1373 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 823, + "2022-03": 824 + } + } + } + ] + } + ] + } + ] + }, + "PR_P1": { + "title": "PR_P1 - Platform Usage", + "x-stoplight": { + "id": "uvq2mrnx11qkt" + }, + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/PR_P1_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PR_P1_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-tags": [ + "PR_P1 - Platform Usage" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "PR_P1", + "Report_Name": "Platform Usage", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Searches_Platform", + "Total_Item_Requests", + "Unique_Item_Requests", + "Unique_Title_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Platform": "Platform 1", + "Attribute_Performance": [ + { + "Data_Type": "Article", + "Performance": { + "Total_Item_Requests": { + "2022-01": 877, + "2022-02": 1022, + "2022-03": 885 + }, + "Unique_Item_Requests": { + "2022-01": 658, + "2022-02": 767, + "2022-03": 664 + } + } + }, + { + "Data_Type": "Audiovisual", + "Performance": { + "Total_Item_Requests": { + "2022-01": 767, + "2022-02": 794, + "2022-03": 1096 + }, + "Unique_Item_Requests": { + "2022-01": 575, + "2022-02": 596, + "2022-03": 822 + } + } + }, + { + "Data_Type": "Book", + "Performance": { + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + }, + { + "Data_Type": "Book_Segment", + "Performance": { + "Total_Item_Requests": { + "2022-01": 694, + "2022-02": 773, + "2022-03": 1093 + }, + "Unique_Item_Requests": { + "2022-01": 521, + "2022-02": 580, + "2022-03": 820 + } + } + }, + { + "Data_Type": "Conference", + "Performance": { + "Total_Item_Requests": { + "2022-01": 752, + "2022-02": 642, + "2022-03": 791 + }, + "Unique_Item_Requests": { + "2022-01": 564, + "2022-02": 482, + "2022-03": 593 + } + } + }, + { + "Data_Type": "Conference_Item", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1126, + "2022-02": 757, + "2022-03": 801 + }, + "Unique_Item_Requests": { + "2022-01": 845, + "2022-02": 568, + "2022-03": 601 + } + } + }, + { + "Data_Type": "Database_Full_Item", + "Performance": { + "Total_Item_Requests": { + "2022-01": 656, + "2022-02": 795, + "2022-03": 1038 + }, + "Unique_Item_Requests": { + "2022-01": 492, + "2022-02": 596, + "2022-03": 779 + } + } + }, + { + "Data_Type": "Dataset", + "Performance": { + "Total_Item_Requests": { + "2022-01": 797, + "2022-02": 901, + "2022-03": 766 + }, + "Unique_Item_Requests": { + "2022-01": 598, + "2022-02": 676, + "2022-03": 575 + } + } + }, + { + "Data_Type": "Image", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1039, + "2022-02": 716, + "2022-03": 1120 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 537, + "2022-03": 840 + } + } + }, + { + "Data_Type": "Interactive_Resource", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1061, + "2022-02": 1168, + "2022-03": 636 + }, + "Unique_Item_Requests": { + "2022-01": 796, + "2022-02": 876, + "2022-03": 477 + } + } + }, + { + "Data_Type": "Journal", + "Performance": { + "Total_Item_Requests": { + "2022-01": 601, + "2022-02": 990, + "2022-03": 974 + }, + "Unique_Item_Requests": { + "2022-01": 451, + "2022-02": 743, + "2022-03": 731 + } + } + }, + { + "Data_Type": "Multimedia", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + }, + { + "Data_Type": "News_Item", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1012, + "2022-02": 1039, + "2022-03": 615 + }, + "Unique_Item_Requests": { + "2022-01": 759, + "2022-02": 779, + "2022-03": 461 + } + } + }, + { + "Data_Type": "Newspaper_or_Newsletter", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1064, + "2022-02": 916, + "2022-03": 820 + }, + "Unique_Item_Requests": { + "2022-01": 798, + "2022-02": 687, + "2022-03": 615 + } + } + }, + { + "Data_Type": "Other", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1038, + "2022-02": 758, + "2022-03": 1072 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 569, + "2022-03": 804 + } + } + }, + { + "Data_Type": "Patent", + "Performance": { + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 644, + "2022-03": 755 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 483, + "2022-03": 566 + } + } + }, + { + "Data_Type": "Platform", + "Performance": { + "Searches_Platform": { + "2022-01": 17484, + "2022-02": 18061, + "2022-03": 17160 + } + } + }, + { + "Data_Type": "Reference_Item", + "Performance": { + "Total_Item_Requests": { + "2022-01": 928, + "2022-02": 845, + "2022-03": 722 + }, + "Unique_Item_Requests": { + "2022-01": 696, + "2022-02": 634, + "2022-03": 542 + } + } + }, + { + "Data_Type": "Reference_Work", + "Performance": { + "Total_Item_Requests": { + "2022-01": 987, + "2022-02": 613, + "2022-03": 664 + }, + "Unique_Item_Requests": { + "2022-01": 740, + "2022-02": 460, + "2022-03": 498 + }, + "Unique_Title_Requests": { + "2022-01": 226, + "2022-02": 212, + "2022-03": 163 + } + } + }, + { + "Data_Type": "Report", + "Performance": { + "Total_Item_Requests": { + "2022-01": 829, + "2022-02": 1111, + "2022-03": 1036 + }, + "Unique_Item_Requests": { + "2022-01": 622, + "2022-02": 833, + "2022-03": 777 + } + } + }, + { + "Data_Type": "Software", + "Performance": { + "Total_Item_Requests": { + "2022-01": 712, + "2022-02": 1065, + "2022-03": 850 + }, + "Unique_Item_Requests": { + "2022-01": 534, + "2022-02": 799, + "2022-03": 638 + } + } + }, + { + "Data_Type": "Sound", + "Performance": { + "Total_Item_Requests": { + "2022-01": 876, + "2022-02": 1143, + "2022-03": 1066 + }, + "Unique_Item_Requests": { + "2022-01": 657, + "2022-02": 857, + "2022-03": 800 + } + } + }, + { + "Data_Type": "Standard", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1080, + "2022-02": 1033, + "2022-03": 751 + }, + "Unique_Item_Requests": { + "2022-01": 810, + "2022-02": 775, + "2022-03": 563 + } + } + }, + { + "Data_Type": "Thesis_or_Dissertation", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1182, + "2022-02": 1158, + "2022-03": 1189 + }, + "Unique_Item_Requests": { + "2022-01": 887, + "2022-02": 869, + "2022-03": 892 + } + } + }, + { + "Data_Type": "Unspecified", + "Performance": { + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 1097, + "2022-03": 1098 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 823, + "2022-03": 824 + } + } + } + ] + } + ] + } + ] + }, + "DR": { + "title": "DR - Database Report", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/DR_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DR_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "e49g5mypmbfu5" + }, + "x-tags": [ + "DR - Database Report" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "DR", + "Report_Name": "Database Report", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Attributes": { + "Attributes_To_Show": [ + "Access_Method" + ] + }, + "Report_Filters": { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + }, + "Report_Items": [ + { + "Database": "Database 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB1" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + }, + { + "Data_Type": "Conference", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1253, + "2022-02": 1070, + "2022-03": 1319 + }, + "Total_Item_Requests": { + "2022-01": 752, + "2022-02": 642, + "2022-03": 791 + }, + "Unique_Item_Investigations": { + "2022-01": 940, + "2022-02": 803, + "2022-03": 989 + }, + "Unique_Item_Requests": { + "2022-01": 564, + "2022-02": 482, + "2022-03": 593 + } + } + }, + { + "Data_Type": "Database_Aggregated", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 47, + "2022-02": 59, + "2022-03": 38 + }, + "No_License": { + "2022-01": 41, + "2022-02": 77, + "2022-03": 42 + }, + "Searches_Automated": { + "2022-01": 15888, + "2022-02": 14802, + "2022-03": 15488 + }, + "Searches_Federated": { + "2022-01": 17178, + "2022-02": 17517, + "2022-03": 18637 + }, + "Searches_Regular": { + "2022-01": 13878, + "2022-02": 13332, + "2022-03": 19412 + } + } + }, + { + "Data_Type": "Journal", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1002, + "2022-02": 1650, + "2022-03": 1623 + }, + "Total_Item_Requests": { + "2022-01": 601, + "2022-02": 990, + "2022-03": 974 + }, + "Unique_Item_Investigations": { + "2022-01": 752, + "2022-02": 1238, + "2022-03": 1217 + }, + "Unique_Item_Requests": { + "2022-01": 451, + "2022-02": 743, + "2022-03": 731 + } + } + }, + { + "Data_Type": "Multimedia", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1865, + "2022-02": 1891, + "2022-03": 1707 + }, + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Investigations": { + "2022-01": 1399, + "2022-02": 1418, + "2022-03": 1280 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + }, + { + "Data_Type": "Newspaper_or_Newsletter", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1773, + "2022-02": 1527, + "2022-03": 1366 + }, + "Total_Item_Requests": { + "2022-01": 1064, + "2022-02": 916, + "2022-03": 820 + }, + "Unique_Item_Investigations": { + "2022-01": 1330, + "2022-02": 1145, + "2022-03": 1025 + }, + "Unique_Item_Requests": { + "2022-01": 798, + "2022-02": 687, + "2022-03": 615 + } + } + }, + { + "Data_Type": "Other", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1730, + "2022-02": 1263, + "2022-03": 1787 + }, + "Total_Item_Requests": { + "2022-01": 1038, + "2022-02": 758, + "2022-03": 1072 + }, + "Unique_Item_Investigations": { + "2022-01": 1298, + "2022-02": 947, + "2022-03": 1340 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 569, + "2022-03": 804 + } + } + }, + { + "Data_Type": "Patent", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1012, + "2022-02": 1074, + "2022-03": 1259 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 644, + "2022-03": 755 + }, + "Unique_Item_Investigations": { + "2022-01": 759, + "2022-02": 806, + "2022-03": 944 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 483, + "2022-03": 566 + } + } + }, + { + "Data_Type": "Reference_Work", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1645, + "2022-02": 1022, + "2022-03": 1106 + }, + "Total_Item_Requests": { + "2022-01": 987, + "2022-02": 613, + "2022-03": 664 + }, + "Unique_Item_Investigations": { + "2022-01": 1234, + "2022-02": 767, + "2022-03": 830 + }, + "Unique_Item_Requests": { + "2022-01": 740, + "2022-02": 460, + "2022-03": 498 + }, + "Unique_Title_Investigations": { + "2022-01": 301, + "2022-02": 283, + "2022-03": 217 + }, + "Unique_Title_Requests": { + "2022-01": 226, + "2022-02": 212, + "2022-03": 163 + } + } + }, + { + "Data_Type": "Report", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1381, + "2022-02": 1852, + "2022-03": 1726 + }, + "Total_Item_Requests": { + "2022-01": 829, + "2022-02": 1111, + "2022-03": 1036 + }, + "Unique_Item_Investigations": { + "2022-01": 1036, + "2022-02": 1389, + "2022-03": 1295 + }, + "Unique_Item_Requests": { + "2022-01": 622, + "2022-02": 833, + "2022-03": 777 + } + } + }, + { + "Data_Type": "Standard", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1800, + "2022-02": 1721, + "2022-03": 1251 + }, + "Total_Item_Requests": { + "2022-01": 1080, + "2022-02": 1033, + "2022-03": 751 + }, + "Unique_Item_Investigations": { + "2022-01": 1350, + "2022-02": 1291, + "2022-03": 938 + }, + "Unique_Item_Requests": { + "2022-01": 810, + "2022-02": 775, + "2022-03": 563 + } + } + }, + { + "Data_Type": "Thesis_or_Dissertation", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1970, + "2022-02": 1930, + "2022-03": 1981 + }, + "Total_Item_Requests": { + "2022-01": 1182, + "2022-02": 1158, + "2022-03": 1189 + }, + "Unique_Item_Investigations": { + "2022-01": 1478, + "2022-02": 1448, + "2022-03": 1486 + }, + "Unique_Item_Requests": { + "2022-01": 887, + "2022-02": 869, + "2022-03": 892 + } + } + }, + { + "Data_Type": "Unspecified", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1011, + "2022-02": 1828, + "2022-03": 1830 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 1097, + "2022-03": 1098 + }, + "Unique_Item_Investigations": { + "2022-01": 758, + "2022-02": 1371, + "2022-03": 1373 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 823, + "2022-03": 824 + } + } + } + ] + }, + { + "Database": "Database 2", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB2" + }, + "Attribute_Performance": [ + { + "Data_Type": "Audiovisual", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1279, + "2022-02": 1323, + "2022-03": 1826 + }, + "Unique_Item_Investigations": { + "2022-01": 959, + "2022-02": 992, + "2022-03": 1370 + } + } + }, + { + "Data_Type": "Database_AI", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 32, + "2022-02": 70, + "2022-03": 24 + }, + "No_License": { + "2022-01": 40, + "2022-02": 54, + "2022-03": 44 + }, + "Searches_Automated": { + "2022-01": 18260, + "2022-02": 15802, + "2022-03": 13441 + }, + "Searches_Federated": { + "2022-01": 16894, + "2022-02": 16869, + "2022-03": 15396 + }, + "Searches_Regular": { + "2022-01": 16221, + "2022-02": 14212, + "2022-03": 18909 + } + } + }, + { + "Data_Type": "Image", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1732, + "2022-02": 1194, + "2022-03": 1867 + }, + "Unique_Item_Investigations": { + "2022-01": 1299, + "2022-02": 896, + "2022-03": 1400 + } + } + }, + { + "Data_Type": "Interactive_Resource", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1769, + "2022-02": 1947, + "2022-03": 1060 + }, + "Unique_Item_Investigations": { + "2022-01": 1327, + "2022-02": 1460, + "2022-03": 795 + } + } + }, + { + "Data_Type": "Sound", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1460, + "2022-02": 1905, + "2022-03": 1777 + }, + "Unique_Item_Investigations": { + "2022-01": 1095, + "2022-02": 1429, + "2022-03": 1333 + } + } + } + ] + }, + { + "Database": "Database 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB3" + }, + "Attribute_Performance": [ + { + "Data_Type": "Database_Full", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 69, + "2022-02": 73, + "2022-03": 67 + }, + "No_License": { + "2022-01": 43, + "2022-02": 12, + "2022-03": 18 + }, + "Searches_Automated": { + "2022-01": 14663, + "2022-02": 19209, + "2022-03": 16943 + }, + "Searches_Federated": { + "2022-01": 17030, + "2022-02": 14920, + "2022-03": 14380 + }, + "Searches_Regular": { + "2022-01": 14784, + "2022-02": 13761, + "2022-03": 15650 + } + } + }, + { + "Data_Type": "Database_Full_Item", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1094, + "2022-02": 1325, + "2022-03": 1730 + }, + "Total_Item_Requests": { + "2022-01": 656, + "2022-02": 795, + "2022-03": 1038 + }, + "Unique_Item_Investigations": { + "2022-01": 821, + "2022-02": 994, + "2022-03": 1298 + }, + "Unique_Item_Requests": { + "2022-01": 492, + "2022-02": 596, + "2022-03": 779 + } + } + } + ] + } + ] + } + ] + }, + "DR_D1": { + "title": "DR_D1 - Database Search and Item Usage", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/DR_D1_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DR_D1_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "wu9gtujat8bvc" + }, + "x-tags": [ + "DR_D1 - Database Search and Item Usage" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "DR_D1", + "Report_Name": "Database Search and Item Usage", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Searches_Automated", + "Searches_Federated", + "Searches_Regular", + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Database": "Database 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB1" + }, + "Attribute_Performance": [ + { + "Performance": { + "Searches_Automated": { + "2022-01": 15888, + "2022-02": 14802, + "2022-03": 15488 + }, + "Searches_Federated": { + "2022-01": 17178, + "2022-02": 17517, + "2022-03": 18637 + }, + "Searches_Regular": { + "2022-01": 13878, + "2022-02": 13332, + "2022-03": 19412 + }, + "Total_Item_Investigations": { + "2022-01": 17546, + "2022-02": 18074, + "2022-03": 18284 + }, + "Total_Item_Requests": { + "2022-01": 10528, + "2022-02": 10845, + "2022-03": 10971 + }, + "Unique_Item_Investigations": { + "2022-01": 13162, + "2022-02": 13558, + "2022-03": 13714 + }, + "Unique_Item_Requests": { + "2022-01": 7897, + "2022-02": 8136, + "2022-03": 8229 + } + } + } + ] + }, + { + "Database": "Database 2", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB2" + }, + "Attribute_Performance": [ + { + "Performance": { + "Searches_Automated": { + "2022-01": 18260, + "2022-02": 15802, + "2022-03": 13441 + }, + "Searches_Federated": { + "2022-01": 16894, + "2022-02": 16869, + "2022-03": 15396 + }, + "Searches_Regular": { + "2022-01": 16221, + "2022-02": 14212, + "2022-03": 18909 + }, + "Total_Item_Investigations": { + "2022-01": 6240, + "2022-02": 6369, + "2022-03": 6530 + }, + "Unique_Item_Investigations": { + "2022-01": 4680, + "2022-02": 4777, + "2022-03": 4898 + } + } + } + ] + }, + { + "Database": "Database 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB3" + }, + "Attribute_Performance": [ + { + "Performance": { + "Searches_Automated": { + "2022-01": 14663, + "2022-02": 19209, + "2022-03": 16943 + }, + "Searches_Federated": { + "2022-01": 17030, + "2022-02": 14920, + "2022-03": 14380 + }, + "Searches_Regular": { + "2022-01": 14784, + "2022-02": 13761, + "2022-03": 15650 + }, + "Total_Item_Investigations": { + "2022-01": 1094, + "2022-02": 1325, + "2022-03": 1730 + }, + "Total_Item_Requests": { + "2022-01": 656, + "2022-02": 795, + "2022-03": 1038 + }, + "Unique_Item_Investigations": { + "2022-01": 821, + "2022-02": 994, + "2022-03": 1298 + }, + "Unique_Item_Requests": { + "2022-01": 492, + "2022-02": 596, + "2022-03": 779 + } + } + } + ] + } + ] + } + ] + }, + "DR_D2": { + "title": "DR_D2 - Database Access Denied", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/DR_D2_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DR_D2_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "filovs2dfo3tr" + }, + "x-tags": [ + "DR_D2 - Database Access Denied" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "DR_D2", + "Report_Name": "Database Access Denied", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Limit_Exceeded", + "No_License" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Database": "Database 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB1" + }, + "Attribute_Performance": [ + { + "Performance": { + "Limit_Exceeded": { + "2022-01": 47, + "2022-02": 59, + "2022-03": 38 + }, + "No_License": { + "2022-01": 41, + "2022-02": 77, + "2022-03": 42 + } + } + } + ] + }, + { + "Database": "Database 2", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB2" + }, + "Attribute_Performance": [ + { + "Performance": { + "Limit_Exceeded": { + "2022-01": 32, + "2022-02": 70, + "2022-03": 24 + }, + "No_License": { + "2022-01": 40, + "2022-02": 54, + "2022-03": 44 + } + } + } + ] + }, + { + "Database": "Database 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB3" + }, + "Attribute_Performance": [ + { + "Performance": { + "Limit_Exceeded": { + "2022-01": 69, + "2022-02": 73, + "2022-03": 67 + }, + "No_License": { + "2022-01": 43, + "2022-02": 12, + "2022-03": 18 + } + } + } + ] + } + ] + } + ] + }, + "TR": { + "title": "TR - Title Report", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/TR_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TR_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "f01a0ad05ijns" + }, + "x-tags": [ + "TR - Title Report" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "TR", + "Report_Name": "Title Report", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Attributes": { + "Attributes_To_Show": [ + "YOP", + "Access_Type", + "Access_Method" + ] + }, + "Report_Filters": { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + }, + "Report_Items": [ + { + "Title": "Title 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt01", + "Proprietary": "P1:T01", + "ISBN": "979-8-88888-888-8", + "URI": "https://doi.org/10.9999/xxxxt01" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 49, + "2022-02": 90, + "2022-03": 40 + }, + "No_License": { + "2022-01": 50, + "2022-02": 84, + "2022-03": 47 + }, + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + } + ] + }, + { + "Title": "Title 2", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt02", + "Proprietary": "P1:T02", + "Online_ISSN": "1234-1234", + "URI": "https://doi.org/10.9999/xxxxt02" + }, + "Attribute_Performance": [ + { + "Data_Type": "Conference", + "YOP": "2021", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 43, + "2022-02": 87, + "2022-03": 35 + }, + "No_License": { + "2022-01": 66, + "2022-02": 73, + "2022-03": 41 + }, + "Total_Item_Investigations": { + "2022-01": 1253, + "2022-02": 1070, + "2022-03": 1319 + }, + "Total_Item_Requests": { + "2022-01": 752, + "2022-02": 642, + "2022-03": 791 + }, + "Unique_Item_Investigations": { + "2022-01": 940, + "2022-02": 803, + "2022-03": 989 + }, + "Unique_Item_Requests": { + "2022-01": 564, + "2022-02": 482, + "2022-03": 593 + } + } + } + ] + }, + { + "Title": "Title 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Attribute_Performance": [ + { + "Data_Type": "Journal", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 87, + "2022-02": 88, + "2022-03": 45 + }, + "No_License": { + "2022-01": 48, + "2022-02": 80, + "2022-03": 37 + }, + "Total_Item_Investigations": { + "2022-01": 250, + "2022-02": 412, + "2022-03": 406 + }, + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Investigations": { + "2022-01": 188, + "2022-02": 309, + "2022-03": 304 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + }, + { + "Data_Type": "Journal", + "YOP": "2022", + "Access_Type": "Open", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 752, + "2022-02": 1238, + "2022-03": 1217 + }, + "Total_Item_Requests": { + "2022-01": 451, + "2022-02": 743, + "2022-03": 731 + }, + "Unique_Item_Investigations": { + "2022-01": 564, + "2022-02": 929, + "2022-03": 913 + }, + "Unique_Item_Requests": { + "2022-01": 338, + "2022-02": 557, + "2022-03": 548 + } + } + }, + { + "Data_Type": "Journal", + "YOP": "2021", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 144, + "2022-02": 145, + "2022-03": 102 + }, + "No_License": { + "2022-01": 105, + "2022-02": 137, + "2022-03": 94 + }, + "Total_Item_Investigations": { + "2022-01": 307, + "2022-02": 469, + "2022-03": 463 + }, + "Total_Item_Requests": { + "2022-01": 207, + "2022-02": 304, + "2022-03": 300 + }, + "Unique_Item_Investigations": { + "2022-01": 245, + "2022-02": 366, + "2022-03": 361 + }, + "Unique_Item_Requests": { + "2022-01": 170, + "2022-02": 243, + "2022-03": 240 + } + } + }, + { + "Data_Type": "Journal", + "YOP": "2021", + "Access_Type": "Open", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 813, + "2022-02": 1299, + "2022-03": 1278 + }, + "Total_Item_Requests": { + "2022-01": 512, + "2022-02": 804, + "2022-03": 792 + }, + "Unique_Item_Investigations": { + "2022-01": 625, + "2022-02": 990, + "2022-03": 974 + }, + "Unique_Item_Requests": { + "2022-01": 399, + "2022-02": 618, + "2022-03": 609 + } + } + } + ] + }, + { + "Title": "Title 4", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt04", + "Proprietary": "P1:T04", + "Online_ISSN": "4321-4321", + "URI": "https://doi.org/10.9999/xxxxt04" + }, + "Attribute_Performance": [ + { + "Data_Type": "Newspaper_or_Newsletter", + "YOP": "2020", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 37, + "2022-02": 75, + "2022-03": 82 + }, + "No_License": { + "2022-01": 87, + "2022-02": 33, + "2022-03": 77 + }, + "Total_Item_Investigations": { + "2022-01": 1773, + "2022-02": 1527, + "2022-03": 1366 + }, + "Total_Item_Requests": { + "2022-01": 1064, + "2022-02": 916, + "2022-03": 820 + }, + "Unique_Item_Investigations": { + "2022-01": 1330, + "2022-02": 1145, + "2022-03": 1025 + }, + "Unique_Item_Requests": { + "2022-01": 798, + "2022-02": 687, + "2022-03": 615 + } + } + } + ] + }, + { + "Title": "Title 5", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt05", + "Proprietary": "P1:T05", + "URI": "https://doi.org/10.9999/xxxxt05" + }, + "Attribute_Performance": [ + { + "Data_Type": "Other", + "YOP": "2019", + "Access_Type": "Free_To_Read", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1730, + "2022-02": 1263, + "2022-03": 1787 + }, + "Total_Item_Requests": { + "2022-01": 1038, + "2022-02": 758, + "2022-03": 1072 + }, + "Unique_Item_Investigations": { + "2022-01": 1298, + "2022-02": 947, + "2022-03": 1340 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 569, + "2022-03": 804 + } + } + } + ] + }, + { + "Title": "Title 6", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt06", + "Proprietary": "P1:T06", + "URI": "https://doi.org/10.9999/xxxxt06" + }, + "Attribute_Performance": [ + { + "Data_Type": "Patent", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 45, + "2022-02": 62, + "2022-03": 35 + }, + "No_License": { + "2022-01": 54, + "2022-02": 66, + "2022-03": 41 + }, + "Total_Item_Investigations": { + "2022-01": 1012, + "2022-02": 1074, + "2022-03": 1259 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 644, + "2022-03": 755 + }, + "Unique_Item_Investigations": { + "2022-01": 759, + "2022-02": 806, + "2022-03": 944 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 483, + "2022-03": 566 + } + } + } + ] + }, + { + "Title": "Title 7", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt07", + "Proprietary": "P1:T07", + "ISBN": "979-8-88888-888-9", + "URI": "https://doi.org/10.9999/xxxxt07" + }, + "Attribute_Performance": [ + { + "Data_Type": "Reference_Work", + "YOP": "2019", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 84, + "2022-02": 84, + "2022-03": 49 + }, + "No_License": { + "2022-01": 70, + "2022-02": 74, + "2022-03": 54 + }, + "Total_Item_Investigations": { + "2022-01": 1645, + "2022-02": 1022, + "2022-03": 1106 + }, + "Total_Item_Requests": { + "2022-01": 987, + "2022-02": 613, + "2022-03": 664 + }, + "Unique_Item_Investigations": { + "2022-01": 1234, + "2022-02": 767, + "2022-03": 830 + }, + "Unique_Item_Requests": { + "2022-01": 740, + "2022-02": 460, + "2022-03": 498 + }, + "Unique_Title_Investigations": { + "2022-01": 301, + "2022-02": 283, + "2022-03": 217 + }, + "Unique_Title_Requests": { + "2022-01": 226, + "2022-02": 212, + "2022-03": 163 + } + } + } + ] + }, + { + "Title": "Title 8", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt08", + "Proprietary": "P1:T08", + "URI": "https://doi.org/10.9999/xxxxt08" + }, + "Attribute_Performance": [ + { + "Data_Type": "Report", + "YOP": "2020", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 61, + "2022-02": 49, + "2022-03": 46 + }, + "No_License": { + "2022-01": 62, + "2022-02": 90, + "2022-03": 70 + }, + "Total_Item_Investigations": { + "2022-01": 1381, + "2022-02": 1852, + "2022-03": 1726 + }, + "Total_Item_Requests": { + "2022-01": 829, + "2022-02": 1111, + "2022-03": 1036 + }, + "Unique_Item_Investigations": { + "2022-01": 1036, + "2022-02": 1389, + "2022-03": 1295 + }, + "Unique_Item_Requests": { + "2022-01": 622, + "2022-02": 833, + "2022-03": 777 + } + } + } + ] + }, + { + "Title": "Title 9", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt09", + "Proprietary": "P1:T09", + "URI": "https://doi.org/10.9999/xxxxt09" + }, + "Attribute_Performance": [ + { + "Data_Type": "Standard", + "YOP": "2022", + "Access_Type": "Free_To_Read", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1800, + "2022-02": 1721, + "2022-03": 1251 + }, + "Total_Item_Requests": { + "2022-01": 1080, + "2022-02": 1033, + "2022-03": 751 + }, + "Unique_Item_Investigations": { + "2022-01": 1350, + "2022-02": 1291, + "2022-03": 938 + }, + "Unique_Item_Requests": { + "2022-01": 810, + "2022-02": 775, + "2022-03": 563 + } + } + } + ] + }, + { + "Title": "Title 10", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt10", + "Proprietary": "P1:T10", + "URI": "https://doi.org/10.9999/xxxxt10" + }, + "Attribute_Performance": [ + { + "Data_Type": "Thesis_or_Dissertation", + "YOP": "2021", + "Access_Type": "Open", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1970, + "2022-02": 1930, + "2022-03": 1981 + }, + "Total_Item_Requests": { + "2022-01": 1182, + "2022-02": 1158, + "2022-03": 1189 + }, + "Unique_Item_Investigations": { + "2022-01": 1478, + "2022-02": 1448, + "2022-03": 1486 + }, + "Unique_Item_Requests": { + "2022-01": 887, + "2022-02": 869, + "2022-03": 892 + } + } + } + ] + }, + { + "Title": "Title 11", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt11", + "Proprietary": "P1:T11", + "URI": "https://doi.org/10.9999/xxxxt11" + }, + "Attribute_Performance": [ + { + "Data_Type": "Unspecified", + "YOP": "2018", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 90, + "2022-02": 87, + "2022-03": 51 + }, + "No_License": { + "2022-01": 80, + "2022-02": 47, + "2022-03": 79 + }, + "Total_Item_Investigations": { + "2022-01": 1011, + "2022-02": 1828, + "2022-03": 1830 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 1097, + "2022-03": 1098 + }, + "Unique_Item_Investigations": { + "2022-01": 758, + "2022-02": 1371, + "2022-03": 1373 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 823, + "2022-03": 824 + } + } + } + ] + } + ] + } + ] + }, + "TR_B1": { + "title": "TR_B1 - Book Requests (Controlled)", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/TR_B1_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TR_B1_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "mxyll4gvm6i6r" + }, + "x-tags": [ + "TR_B1 - Book Requests (Controlled)" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "TR_B1", + "Report_Name": "Book Requests (Controlled)", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Title_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Book", + "Reference_Work" + ], + "Access_Type": [ + "Controlled" + ], + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Title": "Title 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt01", + "Proprietary": "P1:T01", + "ISBN": "979-8-88888-888-8", + "URI": "https://doi.org/10.9999/xxxxt01" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Performance": { + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + } + ] + }, + { + "Title": "Title 7", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt07", + "Proprietary": "P1:T07", + "ISBN": "979-8-88888-888-9", + "URI": "https://doi.org/10.9999/xxxxt07" + }, + "Attribute_Performance": [ + { + "Data_Type": "Reference_Work", + "YOP": "2019", + "Performance": { + "Total_Item_Requests": { + "2022-01": 987, + "2022-02": 613, + "2022-03": 664 + }, + "Unique_Title_Requests": { + "2022-01": 226, + "2022-02": 212, + "2022-03": 163 + } + } + } + ] + } + ] + } + ] + }, + "TR_B2": { + "title": "TR_B2 - Book Access Denied", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/TR_B2_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TR_B2_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "pzcthvk87lnas" + }, + "x-tags": [ + "TR_B2 - Book Access Denied" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "TR_B2", + "Report_Name": "Book Access Denied", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Limit_Exceeded", + "No_License" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Book", + "Reference_Work" + ], + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Title": "Title 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt01", + "Proprietary": "P1:T01", + "ISBN": "979-8-88888-888-8", + "URI": "https://doi.org/10.9999/xxxxt01" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Performance": { + "Limit_Exceeded": { + "2022-01": 49, + "2022-02": 90, + "2022-03": 40 + }, + "No_License": { + "2022-01": 50, + "2022-02": 84, + "2022-03": 47 + } + } + } + ] + }, + { + "Title": "Title 7", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt07", + "Proprietary": "P1:T07", + "ISBN": "979-8-88888-888-9", + "URI": "https://doi.org/10.9999/xxxxt07" + }, + "Attribute_Performance": [ + { + "Data_Type": "Reference_Work", + "YOP": "2019", + "Performance": { + "Limit_Exceeded": { + "2022-01": 84, + "2022-02": 84, + "2022-03": 49 + }, + "No_License": { + "2022-01": 70, + "2022-02": 74, + "2022-03": 54 + } + } + } + ] + } + ] + } + ] + }, + "TR_B3": { + "title": "TR_B3 - Book Usage by Access Type", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/TR_B3_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TR_B3_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "lhg612kxi1s2w" + }, + "x-tags": [ + "TR_B3 - Book Usage by Access Type" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "TR_B3", + "Report_Name": "Book Usage by Access Type", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests", + "Unique_Title_Investigations", + "Unique_Title_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Book", + "Reference_Work" + ], + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Title": "Title 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt01", + "Proprietary": "P1:T01", + "ISBN": "979-8-88888-888-8", + "URI": "https://doi.org/10.9999/xxxxt01" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Access_Type": "Controlled", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + } + ] + }, + { + "Title": "Title 7", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt07", + "Proprietary": "P1:T07", + "ISBN": "979-8-88888-888-9", + "URI": "https://doi.org/10.9999/xxxxt07" + }, + "Attribute_Performance": [ + { + "Data_Type": "Reference_Work", + "YOP": "2019", + "Access_Type": "Controlled", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1645, + "2022-02": 1022, + "2022-03": 1106 + }, + "Total_Item_Requests": { + "2022-01": 987, + "2022-02": 613, + "2022-03": 664 + }, + "Unique_Item_Investigations": { + "2022-01": 1234, + "2022-02": 767, + "2022-03": 830 + }, + "Unique_Item_Requests": { + "2022-01": 740, + "2022-02": 460, + "2022-03": 498 + }, + "Unique_Title_Investigations": { + "2022-01": 301, + "2022-02": 283, + "2022-03": 217 + }, + "Unique_Title_Requests": { + "2022-01": 226, + "2022-02": 212, + "2022-03": 163 + } + } + } + ] + } + ] + } + ] + }, + "TR_J1": { + "title": "TR_J1 - Journal Requests (Controlled)", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/TR_J1_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TR_J1_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "66pewx9iysyuk" + }, + "x-tags": [ + "TR_J1 - Journal Requests (Controlled)" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "TR_J1", + "Report_Name": "Journal Requests (Controlled)", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Type": [ + "Controlled" + ], + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Title": "Title 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Attribute_Performance": [ + { + "Performance": { + "Total_Item_Requests": { + "2022-01": 357, + "2022-02": 551, + "2022-03": 543 + }, + "Unique_Item_Requests": { + "2022-01": 283, + "2022-02": 429, + "2022-03": 423 + } + } + } + ] + } + ] + } + ] + }, + "TR_J2": { + "title": "TR_J2 - Journal Access Denied", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/TR_J2_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TR_J2_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "62ils2miarn72" + }, + "x-tags": [ + "TR_J2 - Journal Access Denied" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "TR_J2", + "Report_Name": "Journal Access Denied", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Limit_Exceeded", + "No_License" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Title": "Title 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Attribute_Performance": [ + { + "Performance": { + "Limit_Exceeded": { + "2022-01": 231, + "2022-02": 233, + "2022-03": 147 + }, + "No_License": { + "2022-01": 153, + "2022-02": 217, + "2022-03": 131 + } + } + } + ] + } + ] + } + ] + }, + "TR_J3": { + "title": "TR_J3 - Journal Usage by Access Type", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/TR_J3_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TR_J3_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "6dfua9i5avrhe" + }, + "x-tags": [ + "TR_J3 - Journal Usage by Access Type" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "TR_J3", + "Report_Name": "Journal Usage by Access Type", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Title": "Title 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Attribute_Performance": [ + { + "Access_Type": "Controlled", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 557, + "2022-02": 881, + "2022-03": 869 + }, + "Total_Item_Requests": { + "2022-01": 357, + "2022-02": 551, + "2022-03": 543 + }, + "Unique_Item_Investigations": { + "2022-01": 433, + "2022-02": 675, + "2022-03": 665 + }, + "Unique_Item_Requests": { + "2022-01": 283, + "2022-02": 429, + "2022-03": 423 + } + } + }, + { + "Access_Type": "Open", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1565, + "2022-02": 2537, + "2022-03": 2495 + }, + "Total_Item_Requests": { + "2022-01": 963, + "2022-02": 1547, + "2022-03": 1523 + }, + "Unique_Item_Investigations": { + "2022-01": 1189, + "2022-02": 1919, + "2022-03": 1887 + }, + "Unique_Item_Requests": { + "2022-01": 737, + "2022-02": 1175, + "2022-03": 1157 + } + } + } + ] + } + ] + } + ] + }, + "TR_J4": { + "title": "TR_J4 - Journal Requests by YOP (Controlled)", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/TR_J4_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TR_J4_Report_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "oddqiv1jfwd88" + }, + "x-tags": [ + "TR_J4 - Journal Requests by YOP (Controlled)" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "TR_J4", + "Report_Name": "Journal Requests by YOP (Controlled)", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Type": [ + "Controlled" + ], + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Title": "Title 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Attribute_Performance": [ + { + "YOP": "2022", + "Performance": { + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + }, + { + "YOP": "2021", + "Performance": { + "Total_Item_Requests": { + "2022-01": 207, + "2022-02": 304, + "2022-03": 300 + }, + "Unique_Item_Requests": { + "2022-01": 170, + "2022-02": 243, + "2022-03": 240 + } + } + } + ] + } + ] + } + ] + }, + "IR": { + "title": "IR - Item Report", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/IR_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/IR_Parent_Item" + }, + { + "$ref": "#/components/schemas/IR_No_Parent_Item" + } + ] + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "96eo0w32fxy2j" + }, + "x-tags": [ + "IR - Item Report" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "IR", + "Report_Name": "Item Report", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Attributes": { + "Attributes_To_Show": [ + "Authors", + "Publication_Date", + "Article_Version", + "YOP", + "Access_Type", + "Access_Method" + ], + "Include_Parent_Details": "True" + }, + "Report_Filters": { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + }, + "Report_Items": [ + { + "Title": "Title 1", + "Data_Type": "Book", + "Item_ID": { + "DOI": "10.9999/xxxxt01", + "Proprietary": "P1:T01", + "ISBN": "979-8-88888-888-8", + "URI": "https://doi.org/10.9999/xxxxt01" + }, + "Items": [ + { + "Item": "Item 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 3" + } + ], + "Publication_Date": "2022-07-24", + "Item_ID": { + "DOI": "10.9999/xxxxi03", + "Proprietary": "P1:I03", + "URI": "https://doi.org/10.9999/xxxxi03" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book_Segment", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 49, + "2022-02": 90, + "2022-03": 40 + }, + "No_License": { + "2022-01": 50, + "2022-02": 84, + "2022-03": 47 + }, + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + } + } + } + ] + } + ] + }, + { + "Title": "Title 2", + "Data_Type": "Conference", + "Item_ID": { + "DOI": "10.9999/xxxxt02", + "Proprietary": "P1:T02", + "Online_ISSN": "1234-1234", + "URI": "https://doi.org/10.9999/xxxxt02" + }, + "Items": [ + { + "Item": "Item 5", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 5" + } + ], + "Publication_Date": "2021-07-25", + "Item_ID": { + "DOI": "10.9999/xxxxi05", + "Proprietary": "P1:I05", + "URI": "https://doi.org/10.9999/xxxxi05" + }, + "Attribute_Performance": [ + { + "Data_Type": "Conference_Item", + "YOP": "2021", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 43, + "2022-02": 87, + "2022-03": 35 + }, + "No_License": { + "2022-01": 66, + "2022-02": 73, + "2022-03": 41 + }, + "Total_Item_Investigations": { + "2022-01": 1253, + "2022-02": 1070, + "2022-03": 1319 + }, + "Total_Item_Requests": { + "2022-01": 752, + "2022-02": 642, + "2022-03": 791 + }, + "Unique_Item_Investigations": { + "2022-01": 940, + "2022-02": 803, + "2022-03": 989 + }, + "Unique_Item_Requests": { + "2022-01": 564, + "2022-02": 482, + "2022-03": 593 + } + } + } + ] + } + ] + }, + { + "Title": "Database 3", + "Data_Type": "Database_Full", + "Item_ID": { + "Proprietary": "P1:DB3" + }, + "Items": [ + { + "Item": "Item 2", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 2" + } + ], + "Publication_Date": "2021-02-21", + "Item_ID": { + "DOI": "10.9999/xxxxi02", + "Proprietary": "P1:I02", + "URI": "https://doi.org/10.9999/xxxxi02" + }, + "Attribute_Performance": [ + { + "Data_Type": "Audiovisual", + "YOP": "2021", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 53, + "2022-02": 53, + "2022-03": 64 + }, + "No_License": { + "2022-01": 30, + "2022-02": 90, + "2022-03": 58 + }, + "Total_Item_Investigations": { + "2022-01": 1279, + "2022-02": 1323, + "2022-03": 1826 + }, + "Total_Item_Requests": { + "2022-01": 767, + "2022-02": 794, + "2022-03": 1096 + }, + "Unique_Item_Investigations": { + "2022-01": 959, + "2022-02": 992, + "2022-03": 1370 + }, + "Unique_Item_Requests": { + "2022-01": 575, + "2022-02": 596, + "2022-03": 822 + } + } + } + ] + }, + { + "Item": "Item 7", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 7" + } + ], + "Publication_Date": "2021-05-28", + "Item_ID": { + "DOI": "10.9999/xxxxi07", + "Proprietary": "P1:I07", + "URI": "https://doi.org/10.9999/xxxxi07" + }, + "Attribute_Performance": [ + { + "Data_Type": "Database_Full_Item", + "YOP": "2021", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 69, + "2022-02": 73, + "2022-03": 67 + }, + "No_License": { + "2022-01": 43, + "2022-02": 12, + "2022-03": 18 + }, + "Total_Item_Investigations": { + "2022-01": 1094, + "2022-02": 1325, + "2022-03": 1730 + }, + "Total_Item_Requests": { + "2022-01": 656, + "2022-02": 795, + "2022-03": 1038 + }, + "Unique_Item_Investigations": { + "2022-01": 821, + "2022-02": 994, + "2022-03": 1298 + }, + "Unique_Item_Requests": { + "2022-01": 492, + "2022-02": 596, + "2022-03": 779 + } + } + } + ] + }, + { + "Item": "Item 9", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 9" + } + ], + "Publication_Date": "2019-08-26", + "Item_ID": { + "DOI": "10.9999/xxxxi09", + "Proprietary": "P1:I09", + "URI": "https://doi.org/10.9999/xxxxi09" + }, + "Attribute_Performance": [ + { + "Data_Type": "Image", + "YOP": "2019", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 45, + "2022-02": 47, + "2022-03": 41 + }, + "No_License": { + "2022-01": 44, + "2022-02": 76, + "2022-03": 64 + }, + "Total_Item_Investigations": { + "2022-01": 1732, + "2022-02": 1194, + "2022-03": 1867 + }, + "Total_Item_Requests": { + "2022-01": 1039, + "2022-02": 716, + "2022-03": 1120 + }, + "Unique_Item_Investigations": { + "2022-01": 1299, + "2022-02": 896, + "2022-03": 1400 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 537, + "2022-03": 840 + } + } + } + ] + }, + { + "Item": "Item 10", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 10" + } + ], + "Publication_Date": "2021-07-20", + "Item_ID": { + "DOI": "10.9999/xxxxi10", + "Proprietary": "P1:I10", + "URI": "https://doi.org/10.9999/xxxxi10" + }, + "Attribute_Performance": [ + { + "Data_Type": "Interactive_Resource", + "YOP": "2021", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 72, + "2022-02": 81, + "2022-03": 66 + }, + "No_License": { + "2022-01": 83, + "2022-02": 43, + "2022-03": 61 + }, + "Total_Item_Investigations": { + "2022-01": 1769, + "2022-02": 1947, + "2022-03": 1060 + }, + "Total_Item_Requests": { + "2022-01": 1061, + "2022-02": 1168, + "2022-03": 636 + }, + "Unique_Item_Investigations": { + "2022-01": 1327, + "2022-02": 1460, + "2022-03": 795 + }, + "Unique_Item_Requests": { + "2022-01": 796, + "2022-02": 876, + "2022-03": 477 + } + } + } + ] + }, + { + "Item": "Item 13", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 13" + } + ], + "Publication_Date": "2020-12-29", + "Item_ID": { + "DOI": "10.9999/xxxxi13", + "Proprietary": "P1:I13", + "URI": "https://doi.org/10.9999/xxxxi13" + }, + "Attribute_Performance": [ + { + "Data_Type": "Multimedia", + "YOP": "2020", + "Access_Type": "Free_To_Read", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1865, + "2022-02": 1891, + "2022-03": 1707 + }, + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Investigations": { + "2022-01": 1399, + "2022-02": 1418, + "2022-03": 1280 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + } + ] + }, + { + "Item": "Item 17", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 17" + } + ], + "Publication_Date": "2022-02-25", + "Item_ID": { + "DOI": "10.9999/xxxxi17", + "Proprietary": "P1:I17", + "URI": "https://doi.org/10.9999/xxxxi17" + }, + "Attribute_Performance": [ + { + "Data_Type": "Patent", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 48, + "2022-02": 90, + "2022-03": 74 + }, + "No_License": { + "2022-01": 48, + "2022-02": 76, + "2022-03": 46 + }, + "Total_Item_Investigations": { + "2022-01": 1012, + "2022-02": 1074, + "2022-03": 1259 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 644, + "2022-03": 755 + }, + "Unique_Item_Investigations": { + "2022-01": 759, + "2022-02": 806, + "2022-03": 944 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 483, + "2022-03": 566 + } + } + } + ] + }, + { + "Item": "Item 20", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 20" + } + ], + "Publication_Date": "2020-02-07", + "Item_ID": { + "DOI": "10.9999/xxxxi20", + "Proprietary": "P1:I20", + "URI": "https://doi.org/10.9999/xxxxi20" + }, + "Attribute_Performance": [ + { + "Data_Type": "Report", + "YOP": "2020", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 77, + "2022-02": 59, + "2022-03": 65 + }, + "No_License": { + "2022-01": 73, + "2022-02": 67, + "2022-03": 62 + }, + "Total_Item_Investigations": { + "2022-01": 1381, + "2022-02": 1852, + "2022-03": 1726 + }, + "Total_Item_Requests": { + "2022-01": 829, + "2022-02": 1111, + "2022-03": 1036 + }, + "Unique_Item_Investigations": { + "2022-01": 1036, + "2022-02": 1389, + "2022-03": 1295 + }, + "Unique_Item_Requests": { + "2022-01": 622, + "2022-02": 833, + "2022-03": 777 + } + } + } + ] + }, + { + "Item": "Item 22", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 22" + } + ], + "Publication_Date": "2021-10-23", + "Item_ID": { + "DOI": "10.9999/xxxxi22", + "Proprietary": "P1:I22", + "URI": "https://doi.org/10.9999/xxxxi22" + }, + "Attribute_Performance": [ + { + "Data_Type": "Sound", + "YOP": "2021", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 32, + "2022-02": 32, + "2022-03": 59 + }, + "No_License": { + "2022-01": 55, + "2022-02": 36, + "2022-03": 41 + }, + "Total_Item_Investigations": { + "2022-01": 1460, + "2022-02": 1905, + "2022-03": 1777 + }, + "Total_Item_Requests": { + "2022-01": 876, + "2022-02": 1143, + "2022-03": 1066 + }, + "Unique_Item_Investigations": { + "2022-01": 1095, + "2022-02": 1429, + "2022-03": 1333 + }, + "Unique_Item_Requests": { + "2022-01": 657, + "2022-02": 857, + "2022-03": 800 + } + } + } + ] + }, + { + "Item": "Item 23", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 23" + } + ], + "Publication_Date": "2022-12-03", + "Item_ID": { + "DOI": "10.9999/xxxxi23", + "Proprietary": "P1:I23", + "URI": "https://doi.org/10.9999/xxxxi23" + }, + "Attribute_Performance": [ + { + "Data_Type": "Standard", + "YOP": "2022", + "Access_Type": "Free_To_Read", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1800, + "2022-02": 1721, + "2022-03": 1251 + }, + "Total_Item_Requests": { + "2022-01": 1080, + "2022-02": 1033, + "2022-03": 751 + }, + "Unique_Item_Investigations": { + "2022-01": 1350, + "2022-02": 1291, + "2022-03": 938 + }, + "Unique_Item_Requests": { + "2022-01": 810, + "2022-02": 775, + "2022-03": 563 + } + } + } + ] + }, + { + "Item": "Item 24", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 24" + } + ], + "Publication_Date": "2021-01-25", + "Item_ID": { + "DOI": "10.9999/xxxxi24", + "Proprietary": "P1:I24", + "URI": "https://doi.org/10.9999/xxxxi24" + }, + "Attribute_Performance": [ + { + "Data_Type": "Thesis_or_Dissertation", + "YOP": "2021", + "Access_Type": "Open", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1970, + "2022-02": 1930, + "2022-03": 1981 + }, + "Total_Item_Requests": { + "2022-01": 1182, + "2022-02": 1158, + "2022-03": 1189 + }, + "Unique_Item_Investigations": { + "2022-01": 1478, + "2022-02": 1448, + "2022-03": 1486 + }, + "Unique_Item_Requests": { + "2022-01": 887, + "2022-02": 869, + "2022-03": 892 + } + } + } + ] + }, + { + "Item": "Item 25", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 25" + } + ], + "Publication_Date": "2018-02-25", + "Item_ID": { + "DOI": "10.9999/xxxxi25", + "Proprietary": "P1:I25", + "URI": "https://doi.org/10.9999/xxxxi25" + }, + "Attribute_Performance": [ + { + "Data_Type": "Unspecified", + "YOP": "2018", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 38, + "2022-02": 84, + "2022-03": 70 + }, + "No_License": { + "2022-01": 66, + "2022-02": 62, + "2022-03": 42 + }, + "Total_Item_Investigations": { + "2022-01": 1011, + "2022-02": 1828, + "2022-03": 1830 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 1097, + "2022-03": 1098 + }, + "Unique_Item_Investigations": { + "2022-01": 758, + "2022-02": 1371, + "2022-03": 1373 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 823, + "2022-03": 824 + } + } + } + ] + } + ] + }, + { + "Title": "Title 3", + "Data_Type": "Journal", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Items": [ + { + "Item": "Item 11", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 11" + } + ], + "Publication_Date": "2022-09-19", + "Item_ID": { + "DOI": "10.9999/xxxxi11", + "Proprietary": "P1:I11", + "URI": "https://doi.org/10.9999/xxxxi11" + }, + "Attribute_Performance": [ + { + "Data_Type": "Article", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 87, + "2022-02": 88, + "2022-03": 45 + }, + "No_License": { + "2022-01": 48, + "2022-02": 80, + "2022-03": 37 + }, + "Total_Item_Investigations": { + "2022-01": 250, + "2022-02": 412, + "2022-03": 406 + }, + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Investigations": { + "2022-01": 188, + "2022-02": 309, + "2022-03": 304 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + } + ] + }, + { + "Item": "Item 12", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 12" + } + ], + "Publication_Date": "2022-10-01", + "Item_ID": { + "DOI": "10.9999/xxxxi12", + "Proprietary": "P1:I12", + "URI": "https://doi.org/10.9999/xxxxi12" + }, + "Attribute_Performance": [ + { + "Data_Type": "Article", + "YOP": "2022", + "Access_Type": "Open", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 752, + "2022-02": 1238, + "2022-03": 1217 + }, + "Total_Item_Requests": { + "2022-01": 451, + "2022-02": 743, + "2022-03": 731 + }, + "Unique_Item_Investigations": { + "2022-01": 564, + "2022-02": 929, + "2022-03": 913 + }, + "Unique_Item_Requests": { + "2022-01": 338, + "2022-02": 557, + "2022-03": 548 + } + } + } + ] + } + ] + }, + { + "Title": "Title 4", + "Data_Type": "Newspaper_or_Newsletter", + "Item_ID": { + "DOI": "10.9999/xxxxt04", + "Proprietary": "P1:T04", + "Online_ISSN": "4321-4321", + "URI": "https://doi.org/10.9999/xxxxt04" + }, + "Items": [ + { + "Item": "Item 15", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 15" + } + ], + "Publication_Date": "2020-01-04", + "Item_ID": { + "DOI": "10.9999/xxxxi15", + "Proprietary": "P1:I15", + "URI": "https://doi.org/10.9999/xxxxi15" + }, + "Attribute_Performance": [ + { + "Data_Type": "News_Item", + "YOP": "2020", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 37, + "2022-02": 75, + "2022-03": 82 + }, + "No_License": { + "2022-01": 87, + "2022-02": 33, + "2022-03": 77 + }, + "Total_Item_Investigations": { + "2022-01": 1773, + "2022-02": 1527, + "2022-03": 1366 + }, + "Total_Item_Requests": { + "2022-01": 1064, + "2022-02": 916, + "2022-03": 820 + }, + "Unique_Item_Investigations": { + "2022-01": 1330, + "2022-02": 1145, + "2022-03": 1025 + }, + "Unique_Item_Requests": { + "2022-01": 798, + "2022-02": 687, + "2022-03": 615 + } + } + } + ] + } + ] + }, + { + "Title": "Title 7", + "Data_Type": "Reference_Work", + "Item_ID": { + "DOI": "10.9999/xxxxt07", + "Proprietary": "P1:T07", + "ISBN": "979-8-88888-888-9", + "URI": "https://doi.org/10.9999/xxxxt07" + }, + "Items": [ + { + "Item": "Item 19", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 19" + } + ], + "Publication_Date": "2019-06-26", + "Item_ID": { + "DOI": "10.9999/xxxxi19", + "Proprietary": "P1:I19", + "URI": "https://doi.org/10.9999/xxxxi19" + }, + "Attribute_Performance": [ + { + "Data_Type": "Reference_Item", + "YOP": "2019", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 84, + "2022-02": 84, + "2022-03": 49 + }, + "No_License": { + "2022-01": 70, + "2022-02": 74, + "2022-03": 54 + }, + "Total_Item_Investigations": { + "2022-01": 1645, + "2022-02": 1022, + "2022-03": 1106 + }, + "Total_Item_Requests": { + "2022-01": 987, + "2022-02": 613, + "2022-03": 664 + }, + "Unique_Item_Investigations": { + "2022-01": 1234, + "2022-02": 767, + "2022-03": 830 + }, + "Unique_Item_Requests": { + "2022-01": 740, + "2022-02": 460, + "2022-03": 498 + } + } + } + ] + } + ] + }, + { + "Items": [ + { + "Item": "Item 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 1" + } + ], + "Publication_Date": "2022-07-19", + "Item_ID": { + "DOI": "10.9999/xxxxi01", + "Proprietary": "P1:I01", + "URI": "https://doi.org/10.9999/xxxxi01" + }, + "Attribute_Performance": [ + { + "Data_Type": "Article", + "YOP": "2022", + "Access_Type": "Open", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1461, + "2022-02": 1704, + "2022-03": 1475 + }, + "Total_Item_Requests": { + "2022-01": 877, + "2022-02": 1022, + "2022-03": 885 + }, + "Unique_Item_Investigations": { + "2022-01": 1096, + "2022-02": 1278, + "2022-03": 1106 + }, + "Unique_Item_Requests": { + "2022-01": 658, + "2022-02": 767, + "2022-03": 664 + } + } + } + ] + }, + { + "Item": "Item 4", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 4" + } + ], + "Publication_Date": "2019-08-17", + "Item_ID": { + "DOI": "10.9999/xxxxi04", + "Proprietary": "P1:I04", + "URI": "https://doi.org/10.9999/xxxxi04" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book_Segment", + "YOP": "2019", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 50, + "2022-02": 43, + "2022-03": 68 + }, + "No_License": { + "2022-01": 33, + "2022-02": 68, + "2022-03": 58 + }, + "Total_Item_Investigations": { + "2022-01": 1156, + "2022-02": 1289, + "2022-03": 1821 + }, + "Total_Item_Requests": { + "2022-01": 694, + "2022-02": 773, + "2022-03": 1093 + }, + "Unique_Item_Investigations": { + "2022-01": 867, + "2022-02": 967, + "2022-03": 1366 + }, + "Unique_Item_Requests": { + "2022-01": 521, + "2022-02": 580, + "2022-03": 820 + } + } + } + ] + }, + { + "Item": "Item 6", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 6" + } + ], + "Publication_Date": "2020-08-09", + "Item_ID": { + "DOI": "10.9999/xxxxi06", + "Proprietary": "P1:I06", + "URI": "https://doi.org/10.9999/xxxxi06" + }, + "Attribute_Performance": [ + { + "Data_Type": "Conference_Item", + "YOP": "2020", + "Access_Type": "Free_To_Read", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1877, + "2022-02": 1261, + "2022-03": 1335 + }, + "Total_Item_Requests": { + "2022-01": 1126, + "2022-02": 757, + "2022-03": 801 + }, + "Unique_Item_Investigations": { + "2022-01": 1408, + "2022-02": 946, + "2022-03": 1001 + }, + "Unique_Item_Requests": { + "2022-01": 845, + "2022-02": 568, + "2022-03": 601 + } + } + } + ] + }, + { + "Item": "Item 8", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 8" + } + ], + "Publication_Date": "2020-09-10", + "Item_ID": { + "DOI": "10.9999/xxxxi08", + "Proprietary": "P1:I08", + "URI": "https://doi.org/10.9999/xxxxi08" + }, + "Attribute_Performance": [ + { + "Data_Type": "Dataset", + "YOP": "2020", + "Access_Type": "Open", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1329, + "2022-02": 1501, + "2022-03": 1276 + }, + "Total_Item_Requests": { + "2022-01": 797, + "2022-02": 901, + "2022-03": 766 + }, + "Unique_Item_Investigations": { + "2022-01": 997, + "2022-02": 1126, + "2022-03": 957 + }, + "Unique_Item_Requests": { + "2022-01": 598, + "2022-02": 676, + "2022-03": 575 + } + } + } + ] + }, + { + "Item": "Item 14", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 14" + } + ], + "Publication_Date": "2018-01-13", + "Item_ID": { + "DOI": "10.9999/xxxxi14", + "Proprietary": "P1:I14", + "URI": "https://doi.org/10.9999/xxxxi14" + }, + "Attribute_Performance": [ + { + "Data_Type": "News_Item", + "YOP": "2018", + "Access_Type": "Free_To_Read", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1687, + "2022-02": 1732, + "2022-03": 1025 + }, + "Total_Item_Requests": { + "2022-01": 1012, + "2022-02": 1039, + "2022-03": 615 + }, + "Unique_Item_Investigations": { + "2022-01": 1265, + "2022-02": 1299, + "2022-03": 769 + }, + "Unique_Item_Requests": { + "2022-01": 759, + "2022-02": 779, + "2022-03": 461 + } + } + } + ] + }, + { + "Item": "Item 16", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 16" + } + ], + "Publication_Date": "2019-11-01", + "Item_ID": { + "DOI": "10.9999/xxxxi16", + "Proprietary": "P1:I16", + "URI": "https://doi.org/10.9999/xxxxi16" + }, + "Attribute_Performance": [ + { + "Data_Type": "Other", + "YOP": "2019", + "Access_Type": "Free_To_Read", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1730, + "2022-02": 1263, + "2022-03": 1787 + }, + "Total_Item_Requests": { + "2022-01": 1038, + "2022-02": 758, + "2022-03": 1072 + }, + "Unique_Item_Investigations": { + "2022-01": 1298, + "2022-02": 947, + "2022-03": 1340 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 569, + "2022-03": 804 + } + } + } + ] + }, + { + "Item": "Item 18", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 18" + } + ], + "Publication_Date": "2021-09-03", + "Item_ID": { + "DOI": "10.9999/xxxxi18", + "Proprietary": "P1:I18", + "URI": "https://doi.org/10.9999/xxxxi18" + }, + "Attribute_Performance": [ + { + "Data_Type": "Reference_Item", + "YOP": "2021", + "Access_Type": "Open", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1546, + "2022-02": 1408, + "2022-03": 1204 + }, + "Total_Item_Requests": { + "2022-01": 928, + "2022-02": 845, + "2022-03": 722 + }, + "Unique_Item_Investigations": { + "2022-01": 1160, + "2022-02": 1056, + "2022-03": 903 + }, + "Unique_Item_Requests": { + "2022-01": 696, + "2022-02": 634, + "2022-03": 542 + } + } + } + ] + }, + { + "Item": "Item 21", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 21" + } + ], + "Publication_Date": "2020-02-08", + "Item_ID": { + "DOI": "10.9999/xxxxi21", + "Proprietary": "P1:I21", + "URI": "https://doi.org/10.9999/xxxxi21" + }, + "Attribute_Performance": [ + { + "Data_Type": "Software", + "YOP": "2020", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 43, + "2022-02": 59, + "2022-03": 38 + }, + "No_License": { + "2022-01": 31, + "2022-02": 44, + "2022-03": 61 + }, + "Total_Item_Investigations": { + "2022-01": 1187, + "2022-02": 1775, + "2022-03": 1416 + }, + "Total_Item_Requests": { + "2022-01": 712, + "2022-02": 1065, + "2022-03": 850 + }, + "Unique_Item_Investigations": { + "2022-01": 890, + "2022-02": 1331, + "2022-03": 1062 + }, + "Unique_Item_Requests": { + "2022-01": 534, + "2022-02": 799, + "2022-03": 638 + } + } + } + ] + } + ] + } + ] + } + ] + }, + "IR_A1": { + "title": "IR_A1 - Journal Article Requests", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/IR_A1_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/IR_A1_Parent_Item" + }, + { + "$ref": "#/components/schemas/IR_A1_No_Parent_Item" + } + ] + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "uck2ahyaa2stm" + }, + "x-tags": [ + "IR_A1 - Journal Article Requests" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "IR_A1", + "Report_Name": "Journal Article Requests", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Article" + ], + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Title": "Title 3", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Items": [ + { + "Item": "Item 11", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 11" + } + ], + "Publication_Date": "2022-09-19", + "Item_ID": { + "DOI": "10.9999/xxxxi11", + "Proprietary": "P1:I11", + "URI": "https://doi.org/10.9999/xxxxi11" + }, + "Attribute_Performance": [ + { + "Access_Type": "Controlled", + "Performance": { + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + } + ] + }, + { + "Item": "Item 12", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 12" + } + ], + "Publication_Date": "2022-10-01", + "Item_ID": { + "DOI": "10.9999/xxxxi12", + "Proprietary": "P1:I12", + "URI": "https://doi.org/10.9999/xxxxi12" + }, + "Attribute_Performance": [ + { + "Access_Type": "Open", + "Performance": { + "Total_Item_Requests": { + "2022-01": 451, + "2022-02": 743, + "2022-03": 731 + }, + "Unique_Item_Requests": { + "2022-01": 338, + "2022-02": 557, + "2022-03": 548 + } + } + } + ] + } + ] + }, + { + "Items": [ + { + "Item": "Item 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 1" + } + ], + "Publication_Date": "2022-07-19", + "Item_ID": { + "DOI": "10.9999/xxxxi01", + "Proprietary": "P1:I01", + "URI": "https://doi.org/10.9999/xxxxi01" + }, + "Attribute_Performance": [ + { + "Access_Type": "Open", + "Performance": { + "Total_Item_Requests": { + "2022-01": 877, + "2022-02": 1022, + "2022-03": 885 + }, + "Unique_Item_Requests": { + "2022-01": 658, + "2022-02": 767, + "2022-03": 664 + } + } + } + ] + } + ] + } + ] + } + ] + }, + "IR_M1": { + "title": "IR_M1 - Multimedia Item Requests", + "type": "object", + "properties": { + "Report_Header": { + "$ref": "#/components/schemas/IR_M1_Report_Header" + }, + "Report_Items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IR_M1_No_Parent_Item" + } + } + }, + "additionalProperties": false, + "required": [ + "Report_Header", + "Report_Items" + ], + "x-stoplight": { + "id": "o2wtd02ggvdy4" + }, + "x-tags": [ + "IR_M1 - Multimedia Item Requests" + ], + "examples": [ + { + "Report_Header": { + "Release": "5.1", + "Report_ID": "IR_M1", + "Report_Name": "Multimedia Item Requests", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Audiovisual", + "Image", + "Interactive_Resource", + "Multimedia", + "Sound" + ], + "Access_Method": [ + "Regular" + ] + } + }, + "Report_Items": [ + { + "Items": [ + { + "Item": "Item 2", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxi02", + "Proprietary": "P1:I02", + "URI": "https://doi.org/10.9999/xxxxi02" + }, + "Attribute_Performance": [ + { + "Data_Type": "Audiovisual", + "Performance": { + "Total_Item_Requests": { + "2022-01": 767, + "2022-02": 794, + "2022-03": 1096 + }, + "Unique_Item_Requests": { + "2022-01": 575, + "2022-02": 596, + "2022-03": 822 + } + } + } + ] + }, + { + "Item": "Item 9", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxi09", + "Proprietary": "P1:I09", + "URI": "https://doi.org/10.9999/xxxxi09" + }, + "Attribute_Performance": [ + { + "Data_Type": "Image", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1039, + "2022-02": 716, + "2022-03": 1120 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 537, + "2022-03": 840 + } + } + } + ] + }, + { + "Item": "Item 10", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxi10", + "Proprietary": "P1:I10", + "URI": "https://doi.org/10.9999/xxxxi10" + }, + "Attribute_Performance": [ + { + "Data_Type": "Interactive_Resource", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1061, + "2022-02": 1168, + "2022-03": 636 + }, + "Unique_Item_Requests": { + "2022-01": 796, + "2022-02": 876, + "2022-03": 477 + } + } + } + ] + }, + { + "Item": "Item 13", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxi13", + "Proprietary": "P1:I13", + "URI": "https://doi.org/10.9999/xxxxi13" + }, + "Attribute_Performance": [ + { + "Data_Type": "Multimedia", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + } + ] + }, + { + "Item": "Item 22", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxi22", + "Proprietary": "P1:I22", + "URI": "https://doi.org/10.9999/xxxxi22" + }, + "Attribute_Performance": [ + { + "Data_Type": "Sound", + "Performance": { + "Total_Item_Requests": { + "2022-01": 876, + "2022-02": 1143, + "2022-03": 1066 + }, + "Unique_Item_Requests": { + "2022-01": 657, + "2022-02": 857, + "2022-03": 800 + } + } + } + ] + } + ] + } + ] + } + ] + }, + "Base_Report_Header": { + "x-stoplight": { + "id": "6tmoun8hivi6v" + }, + "type": "object", + "title": "Base Report_Header", + "examples": [ + { + "Release": "5.1", + "Report_ID": "PR", + "Report_Name": "Platform Report", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Attributes": { + "Attributes_To_Show": [ + "Access_Method" + ] + }, + "Report_Filters": { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + } + ], + "x-tags": [ + "Shared Models" + ], + "properties": { + "Report_Name": { + "type": "string", + "description": "The name of the report." + }, + "Report_ID": { + "type": "string", + "description": "The unique identifier of the report." + }, + "Release": { + "const": "5.1", + "type": "string", + "description": "The COUNTER release this report complies with." + }, + "Institution_Name": { + "type": "string", + "minLength": 2, + "description": "The name of the organization to which the usage is attributed." + }, + "Institution_ID": { + "$ref": "#/components/schemas/Institution_ID", + "description": "A series of identifiers that represent the institution. The customer ID used for requesting the report MUST be included, usually with the platform ID as namespace. For reports to “The World”, Institution_ID should be 0000000000000000, with the platform ID as namespace." + }, + "Report_Filters": { + "type": "object", + "description": "A series of report filters applied on the reported usage, including Begin_Date and End_Date (see the Base_Report_Filters object)." + }, + "Exceptions": { + "type": "array", + "minItems": 1, + "description": "An indication of some difference between the usage that was requested and the usage that is being presented in the report. See the Exception objects for details.", + "uniqueItems": true, + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/Exception_0" + }, + { + "$ref": "#/components/schemas/Exception_1-999" + }, + { + "$ref": "#/components/schemas/Exception_3030" + }, + { + "$ref": "#/components/schemas/Exception_3031" + }, + { + "$ref": "#/components/schemas/Exception_3032" + }, + { + "$ref": "#/components/schemas/Exception_3040" + }, + { + "$ref": "#/components/schemas/Exception_3050" + }, + { + "$ref": "#/components/schemas/Exception_3060" + }, + { + "$ref": "#/components/schemas/Exception_3061" + }, + { + "$ref": "#/components/schemas/Exception_3062" + }, + { + "$ref": "#/components/schemas/Exception_3063" + }, + { + "$ref": "#/components/schemas/Exception_3070" + } + ] + } + }, + "Created": { + "type": "string", + "format": "date-time", + "description": "The date and time the usage was prepared, in RFCRFC3339 date-time format." + }, + "Created_By": { + "type": "string", + "minLength": 2, + "description": "The name of the organization or system that created the COUNTER report." + }, + "Registry_Record": { + "type": "string", + "pattern": "^(https://registry.countermetrics.org/(platform|usage-data-host)/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|)$", + "description": "The link to the platform’s COUNTER Registry record. Report providers who do not have a Registry record MUST leave the value blank." + } + }, + "required": [ + "Report_Name", + "Report_ID", + "Release", + "Institution_Name", + "Institution_ID", + "Report_Filters", + "Created", + "Created_By", + "Registry_Record" + ] + }, + "Base_Report_Filters": { + "x-stoplight": { + "id": "7ny160jujcllt" + }, + "type": "object", + "examples": [ + { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + ], + "properties": { + "Begin_Date": { + "type": "string", + "format": "date" + }, + "End_Date": { + "type": "string", + "format": "date" + }, + "Platform": { + "type": "string", + "minLength": 2 + } + }, + "required": [ + "Begin_Date", + "End_Date" + ], + "title": "Base Report_Filters", + "x-tags": [ + "Shared Models" + ] + }, + "PR_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "PR", + "type": "string" + }, + "Report_Name": { + "const": "Platform Report", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/PR_Report_Filters" + }, + "Report_Attributes": { + "$ref": "#/components/schemas/PR_Report_Attributes" + } + } + } + ], + "unevaluatedProperties": false, + "title": "PR - Report_Header", + "examples": [ + { + "Release": "5.1", + "Report_ID": "PR", + "Report_Name": "Platform Report", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Attributes": { + "Attributes_To_Show": [ + "Access_Method" + ] + }, + "Report_Filters": { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + } + ], + "x-tags": [ + "PR - Platform Report" + ], + "x-stoplight": { + "id": "4ay4bzmz2xm3z" + } + }, + "PR_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "$ref": "#/components/schemas/Access_Method_Filter" + }, + "Data_Type": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Article", + "Audiovisual", + "Book", + "Book_Segment", + "Conference", + "Conference_Item", + "Database_Full_Item", + "Dataset", + "Image", + "Interactive_Resource", + "Journal", + "Multimedia", + "News_Item", + "Newspaper_or_Newsletter", + "Other", + "Patent", + "Platform", + "Reference_Item", + "Reference_Work", + "Report", + "Software", + "Sound", + "Standard", + "Thesis_or_Dissertation", + "Unspecified" + ] + } + }, + "Metric_Type": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Searches_Platform", + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests", + "Unique_Title_Investigations", + "Unique_Title_Requests" + ] + } + } + } + }, + { + "$ref": "#/components/schemas/Common_Extensions_Filters" + } + ], + "unevaluatedProperties": false, + "title": "PR - Report_Filters", + "examples": [ + { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + ], + "x-tags": [ + "PR - Platform Report" + ], + "x-stoplight": { + "id": "1l5jh8a550htj" + } + }, + "PR_Report_Attributes": { + "allOf": [ + { + "type": "object", + "properties": { + "Attributes_To_Show": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Access_Method", + "Institution_Name", + "Customer_ID", + "Country_Name", + "Country_Code", + "Subdivision_Name", + "Subdivision_Code", + "Attributed" + ] + } + } + } + }, + { + "$ref": "#/components/schemas/Granularity_Attribute" + } + ], + "minProperties": 1, + "unevaluatedProperties": false, + "title": "PR - Report_Attributes", + "examples": [ + { + "Attributes_To_Show": [ + "Access_Method" + ] + } + ], + "x-tags": [ + "PR - Platform Report" + ], + "x-stoplight": { + "id": "0ddu2h2x5at5b" + } + }, + "PR_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Platform_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PR_Attribute_Performance_Platform" + }, + { + "$ref": "#/components/schemas/PR_Attribute_Performance_Other" + } + ] + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "PR - Report_Item", + "x-tags": [ + "PR - Platform Report" + ], + "examples": [ + { + "Platform": "Platform 1", + "Attribute_Performance": [ + { + "Data_Type": "Article", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1461, + "2022-02": 1704, + "2022-03": 1475 + }, + "Total_Item_Requests": { + "2022-01": 877, + "2022-02": 1022, + "2022-03": 885 + }, + "Unique_Item_Investigations": { + "2022-01": 1096, + "2022-02": 1278, + "2022-03": 1106 + }, + "Unique_Item_Requests": { + "2022-01": 658, + "2022-02": 767, + "2022-03": 664 + } + } + }, + { + "Data_Type": "Audiovisual", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1279, + "2022-02": 1323, + "2022-03": 1826 + }, + "Total_Item_Requests": { + "2022-01": 767, + "2022-02": 794, + "2022-03": 1096 + }, + "Unique_Item_Investigations": { + "2022-01": 959, + "2022-02": 992, + "2022-03": 1370 + }, + "Unique_Item_Requests": { + "2022-01": 575, + "2022-02": 596, + "2022-03": 822 + } + } + }, + { + "Data_Type": "Book", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + }, + { + "Data_Type": "Book_Segment", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1156, + "2022-02": 1289, + "2022-03": 1821 + }, + "Total_Item_Requests": { + "2022-01": 694, + "2022-02": 773, + "2022-03": 1093 + }, + "Unique_Item_Investigations": { + "2022-01": 867, + "2022-02": 967, + "2022-03": 1366 + }, + "Unique_Item_Requests": { + "2022-01": 521, + "2022-02": 580, + "2022-03": 820 + } + } + }, + { + "Data_Type": "Conference", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1253, + "2022-02": 1070, + "2022-03": 1319 + }, + "Total_Item_Requests": { + "2022-01": 752, + "2022-02": 642, + "2022-03": 791 + }, + "Unique_Item_Investigations": { + "2022-01": 940, + "2022-02": 803, + "2022-03": 989 + }, + "Unique_Item_Requests": { + "2022-01": 564, + "2022-02": 482, + "2022-03": 593 + } + } + }, + { + "Data_Type": "Conference_Item", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1877, + "2022-02": 1261, + "2022-03": 1335 + }, + "Total_Item_Requests": { + "2022-01": 1126, + "2022-02": 757, + "2022-03": 801 + }, + "Unique_Item_Investigations": { + "2022-01": 1408, + "2022-02": 946, + "2022-03": 1001 + }, + "Unique_Item_Requests": { + "2022-01": 845, + "2022-02": 568, + "2022-03": 601 + } + } + }, + { + "Data_Type": "Database_Full_Item", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1094, + "2022-02": 1325, + "2022-03": 1730 + }, + "Total_Item_Requests": { + "2022-01": 656, + "2022-02": 795, + "2022-03": 1038 + }, + "Unique_Item_Investigations": { + "2022-01": 821, + "2022-02": 994, + "2022-03": 1298 + }, + "Unique_Item_Requests": { + "2022-01": 492, + "2022-02": 596, + "2022-03": 779 + } + } + }, + { + "Data_Type": "Dataset", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1329, + "2022-02": 1501, + "2022-03": 1276 + }, + "Total_Item_Requests": { + "2022-01": 797, + "2022-02": 901, + "2022-03": 766 + }, + "Unique_Item_Investigations": { + "2022-01": 997, + "2022-02": 1126, + "2022-03": 957 + }, + "Unique_Item_Requests": { + "2022-01": 598, + "2022-02": 676, + "2022-03": 575 + } + } + }, + { + "Data_Type": "Image", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1732, + "2022-02": 1194, + "2022-03": 1867 + }, + "Total_Item_Requests": { + "2022-01": 1039, + "2022-02": 716, + "2022-03": 1120 + }, + "Unique_Item_Investigations": { + "2022-01": 1299, + "2022-02": 896, + "2022-03": 1400 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 537, + "2022-03": 840 + } + } + }, + { + "Data_Type": "Interactive_Resource", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1769, + "2022-02": 1947, + "2022-03": 1060 + }, + "Total_Item_Requests": { + "2022-01": 1061, + "2022-02": 1168, + "2022-03": 636 + }, + "Unique_Item_Investigations": { + "2022-01": 1327, + "2022-02": 1460, + "2022-03": 795 + }, + "Unique_Item_Requests": { + "2022-01": 796, + "2022-02": 876, + "2022-03": 477 + } + } + }, + { + "Data_Type": "Journal", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1002, + "2022-02": 1650, + "2022-03": 1623 + }, + "Total_Item_Requests": { + "2022-01": 601, + "2022-02": 990, + "2022-03": 974 + }, + "Unique_Item_Investigations": { + "2022-01": 752, + "2022-02": 1238, + "2022-03": 1217 + }, + "Unique_Item_Requests": { + "2022-01": 451, + "2022-02": 743, + "2022-03": 731 + } + } + }, + { + "Data_Type": "Multimedia", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1865, + "2022-02": 1891, + "2022-03": 1707 + }, + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Investigations": { + "2022-01": 1399, + "2022-02": 1418, + "2022-03": 1280 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + }, + { + "Data_Type": "News_Item", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1687, + "2022-02": 1732, + "2022-03": 1025 + }, + "Total_Item_Requests": { + "2022-01": 1012, + "2022-02": 1039, + "2022-03": 615 + }, + "Unique_Item_Investigations": { + "2022-01": 1265, + "2022-02": 1299, + "2022-03": 769 + }, + "Unique_Item_Requests": { + "2022-01": 759, + "2022-02": 779, + "2022-03": 461 + } + } + }, + { + "Data_Type": "Newspaper_or_Newsletter", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1773, + "2022-02": 1527, + "2022-03": 1366 + }, + "Total_Item_Requests": { + "2022-01": 1064, + "2022-02": 916, + "2022-03": 820 + }, + "Unique_Item_Investigations": { + "2022-01": 1330, + "2022-02": 1145, + "2022-03": 1025 + }, + "Unique_Item_Requests": { + "2022-01": 798, + "2022-02": 687, + "2022-03": 615 + } + } + }, + { + "Data_Type": "Other", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1730, + "2022-02": 1263, + "2022-03": 1787 + }, + "Total_Item_Requests": { + "2022-01": 1038, + "2022-02": 758, + "2022-03": 1072 + }, + "Unique_Item_Investigations": { + "2022-01": 1298, + "2022-02": 947, + "2022-03": 1340 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 569, + "2022-03": 804 + } + } + }, + { + "Data_Type": "Patent", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1012, + "2022-02": 1074, + "2022-03": 1259 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 644, + "2022-03": 755 + }, + "Unique_Item_Investigations": { + "2022-01": 759, + "2022-02": 806, + "2022-03": 944 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 483, + "2022-03": 566 + } + } + }, + { + "Data_Type": "Platform", + "Access_Method": "Regular", + "Performance": { + "Searches_Platform": { + "2022-01": 17484, + "2022-02": 18061, + "2022-03": 17160 + } + } + }, + { + "Data_Type": "Reference_Item", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1546, + "2022-02": 1408, + "2022-03": 1204 + }, + "Total_Item_Requests": { + "2022-01": 928, + "2022-02": 845, + "2022-03": 722 + }, + "Unique_Item_Investigations": { + "2022-01": 1160, + "2022-02": 1056, + "2022-03": 903 + }, + "Unique_Item_Requests": { + "2022-01": 696, + "2022-02": 634, + "2022-03": 542 + } + } + }, + { + "Data_Type": "Reference_Work", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1645, + "2022-02": 1022, + "2022-03": 1106 + }, + "Total_Item_Requests": { + "2022-01": 987, + "2022-02": 613, + "2022-03": 664 + }, + "Unique_Item_Investigations": { + "2022-01": 1234, + "2022-02": 767, + "2022-03": 830 + }, + "Unique_Item_Requests": { + "2022-01": 740, + "2022-02": 460, + "2022-03": 498 + }, + "Unique_Title_Investigations": { + "2022-01": 301, + "2022-02": 283, + "2022-03": 217 + }, + "Unique_Title_Requests": { + "2022-01": 226, + "2022-02": 212, + "2022-03": 163 + } + } + }, + { + "Data_Type": "Report", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1381, + "2022-02": 1852, + "2022-03": 1726 + }, + "Total_Item_Requests": { + "2022-01": 829, + "2022-02": 1111, + "2022-03": 1036 + }, + "Unique_Item_Investigations": { + "2022-01": 1036, + "2022-02": 1389, + "2022-03": 1295 + }, + "Unique_Item_Requests": { + "2022-01": 622, + "2022-02": 833, + "2022-03": 777 + } + } + }, + { + "Data_Type": "Software", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1187, + "2022-02": 1775, + "2022-03": 1416 + }, + "Total_Item_Requests": { + "2022-01": 712, + "2022-02": 1065, + "2022-03": 850 + }, + "Unique_Item_Investigations": { + "2022-01": 890, + "2022-02": 1331, + "2022-03": 1062 + }, + "Unique_Item_Requests": { + "2022-01": 534, + "2022-02": 799, + "2022-03": 638 + } + } + }, + { + "Data_Type": "Sound", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1460, + "2022-02": 1905, + "2022-03": 1777 + }, + "Total_Item_Requests": { + "2022-01": 876, + "2022-02": 1143, + "2022-03": 1066 + }, + "Unique_Item_Investigations": { + "2022-01": 1095, + "2022-02": 1429, + "2022-03": 1333 + }, + "Unique_Item_Requests": { + "2022-01": 657, + "2022-02": 857, + "2022-03": 800 + } + } + }, + { + "Data_Type": "Standard", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1800, + "2022-02": 1721, + "2022-03": 1251 + }, + "Total_Item_Requests": { + "2022-01": 1080, + "2022-02": 1033, + "2022-03": 751 + }, + "Unique_Item_Investigations": { + "2022-01": 1350, + "2022-02": 1291, + "2022-03": 938 + }, + "Unique_Item_Requests": { + "2022-01": 810, + "2022-02": 775, + "2022-03": 563 + } + } + }, + { + "Data_Type": "Thesis_or_Dissertation", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1970, + "2022-02": 1930, + "2022-03": 1981 + }, + "Total_Item_Requests": { + "2022-01": 1182, + "2022-02": 1158, + "2022-03": 1189 + }, + "Unique_Item_Investigations": { + "2022-01": 1478, + "2022-02": 1448, + "2022-03": 1486 + }, + "Unique_Item_Requests": { + "2022-01": 887, + "2022-02": 869, + "2022-03": 892 + } + } + }, + { + "Data_Type": "Unspecified", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1011, + "2022-02": 1828, + "2022-03": 1830 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 1097, + "2022-03": 1098 + }, + "Unique_Item_Investigations": { + "2022-01": 758, + "2022-02": 1371, + "2022-03": 1373 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 823, + "2022-03": 824 + } + } + } + ] + } + ], + "x-stoplight": { + "id": "p5nvx33xdi9kg" + } + }, + "PR_Attribute_Performance_Platform": { + "allOf": [ + { + "type": "object", + "properties": { + "Data_Type": { + "type": "string", + "const": "Platform" + }, + "Access_Method": { + "$ref": "#/components/schemas/Access_Method_Attribute" + }, + "Performance": { + "$ref": "#/components/schemas/PR_Performance_Platform" + } + }, + "required": [ + "Data_Type", + "Performance" + ] + }, + { + "$ref": "#/components/schemas/Common_Extensions_Attributes" + } + ], + "unevaluatedProperties": false, + "x-tags": [ + "PR - Platform Report" + ], + "title": "PR - Attribute_Performance Platform", + "examples": [ + { + "Data_Type": "Platform", + "Access_Method": "Regular", + "Performance": { + "Searches_Platform": { + "2022-01": 17484, + "2022-02": 18061, + "2022-03": 17160 + } + } + } + ], + "x-stoplight": { + "id": "ri5wodp6cs5nu" + } + }, + "PR_Attribute_Performance_Other": { + "allOf": [ + { + "type": "object", + "properties": { + "Data_Type": { + "type": "string", + "enum": [ + "Article", + "Audiovisual", + "Book", + "Book_Segment", + "Conference", + "Conference_Item", + "Database_Full_Item", + "Dataset", + "Image", + "Interactive_Resource", + "Journal", + "Multimedia", + "News_Item", + "Newspaper_or_Newsletter", + "Other", + "Patent", + "Reference_Item", + "Reference_Work", + "Report", + "Software", + "Sound", + "Standard", + "Thesis_or_Dissertation", + "Unspecified" + ] + }, + "Access_Method": { + "$ref": "#/components/schemas/Access_Method_Attribute" + }, + "Performance": { + "$ref": "#/components/schemas/PR_Performance_Other" + } + }, + "required": [ + "Data_Type", + "Performance" + ] + }, + { + "$ref": "#/components/schemas/Common_Extensions_Attributes" + } + ], + "unevaluatedProperties": false, + "x-tags": [ + "PR - Platform Report" + ], + "title": "PR - Attribute_Performance Other", + "examples": [ + { + "Data_Type": "Book", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + } + ], + "x-stoplight": { + "id": "97nd9tj2ybhvb" + } + }, + "PR_Performance_Platform": { + "title": "PR - Performance Platform", + "examples": [ + { + "Searches_Platform": { + "2022-01": 17484, + "2022-02": 18061, + "2022-03": 17160 + } + } + ], + "type": "object", + "properties": { + "Searches_Platform": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 1, + "additionalProperties": false, + "x-stoplight": { + "id": "qb5cdngpy9kw2" + }, + "x-tags": [ + "PR - Platform Report" + ] + }, + "PR_Performance_Other": { + "title": "PR - Performance Other", + "examples": [ + { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + ], + "type": "object", + "properties": { + "Total_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Title_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Title_Requests": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-stoplight": { + "id": "hhnp0hyqbe2nf" + }, + "x-tags": [ + "PR - Platform Report" + ] + }, + "PR_P1_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "PR_P1", + "type": "string" + }, + "Report_Name": { + "const": "Platform Usage", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/PR_P1_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "PR_P1 - Report_Header", + "examples": [ + { + "Release": "5.1", + "Report_ID": "PR_P1", + "Report_Name": "Platform Usage", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Searches_Platform", + "Total_Item_Requests", + "Unique_Item_Requests", + "Unique_Title_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "PR_P1 - Platform Usage" + ], + "x-stoplight": { + "id": "0xavrbb9dcd16" + } + }, + "PR_P1_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Metric_Type": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Searches_Platform", + "Total_Item_Requests", + "Unique_Item_Requests", + "Unique_Title_Requests" + ] + } + } + }, + "required": [ + "Access_Method", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "PR_P1 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Searches_Platform", + "Total_Item_Requests", + "Unique_Item_Requests", + "Unique_Title_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Access_Method": [ + "Regular" + ] + } + ], + "x-tags": [ + "PR_P1 - Platform Usage" + ], + "x-stoplight": { + "id": "oyshu9qe9aeof" + } + }, + "PR_P1_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Platform_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PR_P1_Attribute_Performance_Platform" + }, + { + "$ref": "#/components/schemas/PR_P1_Attribute_Performance_Other" + } + ] + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "PR_P1 - Report_Item", + "x-tags": [ + "PR_P1 - Platform Usage" + ], + "examples": [ + { + "Platform": "Platform 1", + "Attribute_Performance": [ + { + "Data_Type": "Article", + "Performance": { + "Total_Item_Requests": { + "2022-01": 877, + "2022-02": 1022, + "2022-03": 885 + }, + "Unique_Item_Requests": { + "2022-01": 658, + "2022-02": 767, + "2022-03": 664 + } + } + }, + { + "Data_Type": "Audiovisual", + "Performance": { + "Total_Item_Requests": { + "2022-01": 767, + "2022-02": 794, + "2022-03": 1096 + }, + "Unique_Item_Requests": { + "2022-01": 575, + "2022-02": 596, + "2022-03": 822 + } + } + }, + { + "Data_Type": "Book", + "Performance": { + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + }, + { + "Data_Type": "Book_Segment", + "Performance": { + "Total_Item_Requests": { + "2022-01": 694, + "2022-02": 773, + "2022-03": 1093 + }, + "Unique_Item_Requests": { + "2022-01": 521, + "2022-02": 580, + "2022-03": 820 + } + } + }, + { + "Data_Type": "Conference", + "Performance": { + "Total_Item_Requests": { + "2022-01": 752, + "2022-02": 642, + "2022-03": 791 + }, + "Unique_Item_Requests": { + "2022-01": 564, + "2022-02": 482, + "2022-03": 593 + } + } + }, + { + "Data_Type": "Conference_Item", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1126, + "2022-02": 757, + "2022-03": 801 + }, + "Unique_Item_Requests": { + "2022-01": 845, + "2022-02": 568, + "2022-03": 601 + } + } + }, + { + "Data_Type": "Database_Full_Item", + "Performance": { + "Total_Item_Requests": { + "2022-01": 656, + "2022-02": 795, + "2022-03": 1038 + }, + "Unique_Item_Requests": { + "2022-01": 492, + "2022-02": 596, + "2022-03": 779 + } + } + }, + { + "Data_Type": "Dataset", + "Performance": { + "Total_Item_Requests": { + "2022-01": 797, + "2022-02": 901, + "2022-03": 766 + }, + "Unique_Item_Requests": { + "2022-01": 598, + "2022-02": 676, + "2022-03": 575 + } + } + }, + { + "Data_Type": "Image", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1039, + "2022-02": 716, + "2022-03": 1120 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 537, + "2022-03": 840 + } + } + }, + { + "Data_Type": "Interactive_Resource", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1061, + "2022-02": 1168, + "2022-03": 636 + }, + "Unique_Item_Requests": { + "2022-01": 796, + "2022-02": 876, + "2022-03": 477 + } + } + }, + { + "Data_Type": "Journal", + "Performance": { + "Total_Item_Requests": { + "2022-01": 601, + "2022-02": 990, + "2022-03": 974 + }, + "Unique_Item_Requests": { + "2022-01": 451, + "2022-02": 743, + "2022-03": 731 + } + } + }, + { + "Data_Type": "Multimedia", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + }, + { + "Data_Type": "News_Item", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1012, + "2022-02": 1039, + "2022-03": 615 + }, + "Unique_Item_Requests": { + "2022-01": 759, + "2022-02": 779, + "2022-03": 461 + } + } + }, + { + "Data_Type": "Newspaper_or_Newsletter", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1064, + "2022-02": 916, + "2022-03": 820 + }, + "Unique_Item_Requests": { + "2022-01": 798, + "2022-02": 687, + "2022-03": 615 + } + } + }, + { + "Data_Type": "Other", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1038, + "2022-02": 758, + "2022-03": 1072 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 569, + "2022-03": 804 + } + } + }, + { + "Data_Type": "Patent", + "Performance": { + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 644, + "2022-03": 755 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 483, + "2022-03": 566 + } + } + }, + { + "Data_Type": "Platform", + "Performance": { + "Searches_Platform": { + "2022-01": 17484, + "2022-02": 18061, + "2022-03": 17160 + } + } + }, + { + "Data_Type": "Reference_Item", + "Performance": { + "Total_Item_Requests": { + "2022-01": 928, + "2022-02": 845, + "2022-03": 722 + }, + "Unique_Item_Requests": { + "2022-01": 696, + "2022-02": 634, + "2022-03": 542 + } + } + }, + { + "Data_Type": "Reference_Work", + "Performance": { + "Total_Item_Requests": { + "2022-01": 987, + "2022-02": 613, + "2022-03": 664 + }, + "Unique_Item_Requests": { + "2022-01": 740, + "2022-02": 460, + "2022-03": 498 + }, + "Unique_Title_Requests": { + "2022-01": 226, + "2022-02": 212, + "2022-03": 163 + } + } + }, + { + "Data_Type": "Report", + "Performance": { + "Total_Item_Requests": { + "2022-01": 829, + "2022-02": 1111, + "2022-03": 1036 + }, + "Unique_Item_Requests": { + "2022-01": 622, + "2022-02": 833, + "2022-03": 777 + } + } + }, + { + "Data_Type": "Software", + "Performance": { + "Total_Item_Requests": { + "2022-01": 712, + "2022-02": 1065, + "2022-03": 850 + }, + "Unique_Item_Requests": { + "2022-01": 534, + "2022-02": 799, + "2022-03": 638 + } + } + }, + { + "Data_Type": "Sound", + "Performance": { + "Total_Item_Requests": { + "2022-01": 876, + "2022-02": 1143, + "2022-03": 1066 + }, + "Unique_Item_Requests": { + "2022-01": 657, + "2022-02": 857, + "2022-03": 800 + } + } + }, + { + "Data_Type": "Standard", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1080, + "2022-02": 1033, + "2022-03": 751 + }, + "Unique_Item_Requests": { + "2022-01": 810, + "2022-02": 775, + "2022-03": 563 + } + } + }, + { + "Data_Type": "Thesis_or_Dissertation", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1182, + "2022-02": 1158, + "2022-03": 1189 + }, + "Unique_Item_Requests": { + "2022-01": 887, + "2022-02": 869, + "2022-03": 892 + } + } + }, + { + "Data_Type": "Unspecified", + "Performance": { + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 1097, + "2022-03": 1098 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 823, + "2022-03": 824 + } + } + } + ] + } + ], + "x-stoplight": { + "id": "v7aifgvzcu1cm" + } + }, + "PR_P1_Attribute_Performance_Platform": { + "title": "PR_P1 - Attribute_Performance Platform", + "examples": [ + { + "Data_Type": "Platform", + "Performance": { + "Searches_Platform": { + "2022-01": 17484, + "2022-02": 18061, + "2022-03": 17160 + } + } + } + ], + "x-stoplight": { + "id": "2zve3xzverims" + }, + "type": "object", + "properties": { + "Data_Type": { + "type": "string", + "const": "Platform" + }, + "Performance": { + "$ref": "#/components/schemas/PR_P1_Performance_Platform" + } + }, + "required": [ + "Data_Type", + "Performance" + ], + "additionalProperties": false, + "x-tags": [ + "PR_P1 - Platform Usage" + ] + }, + "PR_P1_Attribute_Performance_Other": { + "title": "PR_P1 - Attribute_Performance Other", + "examples": [ + { + "Data_Type": "Book", + "Performance": { + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + } + ], + "x-stoplight": { + "id": "72cj2gqm2pgkg" + }, + "type": "object", + "properties": { + "Data_Type": { + "type": "string", + "enum": [ + "Article", + "Audiovisual", + "Book", + "Book_Segment", + "Conference", + "Conference_Item", + "Database_Full_Item", + "Dataset", + "Image", + "Interactive_Resource", + "Journal", + "Multimedia", + "News_Item", + "Newspaper_or_Newsletter", + "Other", + "Patent", + "Reference_Item", + "Reference_Work", + "Report", + "Software", + "Sound", + "Standard", + "Thesis_or_Dissertation", + "Unspecified" + ] + }, + "Performance": { + "$ref": "#/components/schemas/PR_P1_Performance_Other" + } + }, + "required": [ + "Data_Type", + "Performance" + ], + "additionalProperties": false, + "x-tags": [ + "PR_P1 - Platform Usage" + ] + }, + "PR_P1_Performance_Platform": { + "title": "PR_P1 - Performance Platform", + "examples": [ + { + "Searches_Platform": { + "2022-01": 17484, + "2022-02": 18061, + "2022-03": 17160 + } + } + ], + "x-stoplight": { + "id": "jcfe1169gz2c1" + }, + "type": "object", + "properties": { + "Searches_Platform": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 1, + "additionalProperties": false, + "x-tags": [ + "PR_P1 - Platform Usage" + ] + }, + "PR_P1_Performance_Other": { + "title": "PR_P1 - Performance Other", + "examples": [ + { + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + ], + "x-stoplight": { + "id": "2kvprp5jwf1d0" + }, + "type": "object", + "properties": { + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Title_Requests": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-tags": [ + "PR_P1 - Platform Usage" + ] + }, + "DR_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "DR", + "type": "string" + }, + "Report_Name": { + "const": "Database Report", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/DR_Report_Filters" + }, + "Report_Attributes": { + "$ref": "#/components/schemas/DR_Report_Attributes" + } + } + } + ], + "unevaluatedProperties": false, + "title": "DR - Report_Header", + "x-stoplight": { + "id": "h74l5s2euko00" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "DR", + "Report_Name": "Database Report", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Attributes": { + "Attributes_To_Show": [ + "Access_Method" + ] + }, + "Report_Filters": { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + } + ], + "x-tags": [ + "DR - Database Report" + ] + }, + "DR_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Database": { + "type": "string", + "minLength": 2 + }, + "Access_Method": { + "$ref": "#/components/schemas/Access_Method_Filter" + }, + "Data_Type": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Audiovisual", + "Book", + "Conference", + "Database_Aggregated", + "Database_AI", + "Database_Full", + "Database_Full_Item", + "Image", + "Interactive_Resource", + "Journal", + "Multimedia", + "Newspaper_or_Newsletter", + "Other", + "Patent", + "Reference_Work", + "Report", + "Sound", + "Standard", + "Thesis_or_Dissertation", + "Unspecified" + ] + } + }, + "Metric_Type": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Searches_Automated", + "Searches_Federated", + "Searches_Regular", + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests", + "Unique_Title_Investigations", + "Unique_Title_Requests", + "Limit_Exceeded", + "No_License" + ] + } + } + } + }, + { + "$ref": "#/components/schemas/Common_Extensions_Filters" + } + ], + "unevaluatedProperties": false, + "title": "DR - Report_Filters", + "examples": [ + { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + ], + "x-stoplight": { + "id": "c0je2emycob8w" + }, + "x-tags": [ + "DR - Database Report" + ] + }, + "DR_Report_Attributes": { + "allOf": [ + { + "type": "object", + "properties": { + "Attributes_To_Show": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Access_Method", + "Institution_Name", + "Customer_ID", + "Country_Name", + "Country_Code", + "Subdivision_Name", + "Subdivision_Code", + "Attributed" + ] + } + } + } + }, + { + "$ref": "#/components/schemas/Granularity_Attribute" + } + ], + "minProperties": 1, + "unevaluatedProperties": false, + "title": "DR - Report_Attributes", + "examples": [ + { + "Attributes_To_Show": [ + "Access_Method" + ] + } + ], + "x-stoplight": { + "id": "0752xn5saf6fq" + }, + "x-tags": [ + "DR - Database Report" + ] + }, + "DR_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Database_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DR_Attribute_Performance_Database" + }, + { + "$ref": "#/components/schemas/DR_Attribute_Performance_Other" + } + ] + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "x-stoplight": { + "id": "brfjywrzpgofd" + }, + "title": "DR - Report_Item", + "x-tags": [ + "DR - Database Report" + ], + "examples": [ + { + "Database": "Database 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB1" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + }, + { + "Data_Type": "Conference", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1253, + "2022-02": 1070, + "2022-03": 1319 + }, + "Total_Item_Requests": { + "2022-01": 752, + "2022-02": 642, + "2022-03": 791 + }, + "Unique_Item_Investigations": { + "2022-01": 940, + "2022-02": 803, + "2022-03": 989 + }, + "Unique_Item_Requests": { + "2022-01": 564, + "2022-02": 482, + "2022-03": 593 + } + } + }, + { + "Data_Type": "Database_Aggregated", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 47, + "2022-02": 59, + "2022-03": 38 + }, + "No_License": { + "2022-01": 41, + "2022-02": 77, + "2022-03": 42 + }, + "Searches_Automated": { + "2022-01": 15888, + "2022-02": 14802, + "2022-03": 15488 + }, + "Searches_Federated": { + "2022-01": 17178, + "2022-02": 17517, + "2022-03": 18637 + }, + "Searches_Regular": { + "2022-01": 13878, + "2022-02": 13332, + "2022-03": 19412 + } + } + }, + { + "Data_Type": "Journal", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1002, + "2022-02": 1650, + "2022-03": 1623 + }, + "Total_Item_Requests": { + "2022-01": 601, + "2022-02": 990, + "2022-03": 974 + }, + "Unique_Item_Investigations": { + "2022-01": 752, + "2022-02": 1238, + "2022-03": 1217 + }, + "Unique_Item_Requests": { + "2022-01": 451, + "2022-02": 743, + "2022-03": 731 + } + } + }, + { + "Data_Type": "Multimedia", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1865, + "2022-02": 1891, + "2022-03": 1707 + }, + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Investigations": { + "2022-01": 1399, + "2022-02": 1418, + "2022-03": 1280 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + }, + { + "Data_Type": "Newspaper_or_Newsletter", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1773, + "2022-02": 1527, + "2022-03": 1366 + }, + "Total_Item_Requests": { + "2022-01": 1064, + "2022-02": 916, + "2022-03": 820 + }, + "Unique_Item_Investigations": { + "2022-01": 1330, + "2022-02": 1145, + "2022-03": 1025 + }, + "Unique_Item_Requests": { + "2022-01": 798, + "2022-02": 687, + "2022-03": 615 + } + } + }, + { + "Data_Type": "Other", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1730, + "2022-02": 1263, + "2022-03": 1787 + }, + "Total_Item_Requests": { + "2022-01": 1038, + "2022-02": 758, + "2022-03": 1072 + }, + "Unique_Item_Investigations": { + "2022-01": 1298, + "2022-02": 947, + "2022-03": 1340 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 569, + "2022-03": 804 + } + } + }, + { + "Data_Type": "Patent", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1012, + "2022-02": 1074, + "2022-03": 1259 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 644, + "2022-03": 755 + }, + "Unique_Item_Investigations": { + "2022-01": 759, + "2022-02": 806, + "2022-03": 944 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 483, + "2022-03": 566 + } + } + }, + { + "Data_Type": "Reference_Work", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1645, + "2022-02": 1022, + "2022-03": 1106 + }, + "Total_Item_Requests": { + "2022-01": 987, + "2022-02": 613, + "2022-03": 664 + }, + "Unique_Item_Investigations": { + "2022-01": 1234, + "2022-02": 767, + "2022-03": 830 + }, + "Unique_Item_Requests": { + "2022-01": 740, + "2022-02": 460, + "2022-03": 498 + }, + "Unique_Title_Investigations": { + "2022-01": 301, + "2022-02": 283, + "2022-03": 217 + }, + "Unique_Title_Requests": { + "2022-01": 226, + "2022-02": 212, + "2022-03": 163 + } + } + }, + { + "Data_Type": "Report", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1381, + "2022-02": 1852, + "2022-03": 1726 + }, + "Total_Item_Requests": { + "2022-01": 829, + "2022-02": 1111, + "2022-03": 1036 + }, + "Unique_Item_Investigations": { + "2022-01": 1036, + "2022-02": 1389, + "2022-03": 1295 + }, + "Unique_Item_Requests": { + "2022-01": 622, + "2022-02": 833, + "2022-03": 777 + } + } + }, + { + "Data_Type": "Standard", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1800, + "2022-02": 1721, + "2022-03": 1251 + }, + "Total_Item_Requests": { + "2022-01": 1080, + "2022-02": 1033, + "2022-03": 751 + }, + "Unique_Item_Investigations": { + "2022-01": 1350, + "2022-02": 1291, + "2022-03": 938 + }, + "Unique_Item_Requests": { + "2022-01": 810, + "2022-02": 775, + "2022-03": 563 + } + } + }, + { + "Data_Type": "Thesis_or_Dissertation", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1970, + "2022-02": 1930, + "2022-03": 1981 + }, + "Total_Item_Requests": { + "2022-01": 1182, + "2022-02": 1158, + "2022-03": 1189 + }, + "Unique_Item_Investigations": { + "2022-01": 1478, + "2022-02": 1448, + "2022-03": 1486 + }, + "Unique_Item_Requests": { + "2022-01": 887, + "2022-02": 869, + "2022-03": 892 + } + } + }, + { + "Data_Type": "Unspecified", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1011, + "2022-02": 1828, + "2022-03": 1830 + }, + "Total_Item_Requests": { + "2022-01": 607, + "2022-02": 1097, + "2022-03": 1098 + }, + "Unique_Item_Investigations": { + "2022-01": 758, + "2022-02": 1371, + "2022-03": 1373 + }, + "Unique_Item_Requests": { + "2022-01": 455, + "2022-02": 823, + "2022-03": 824 + } + } + } + ] + } + ] + }, + "DR_Attribute_Performance_Database": { + "allOf": [ + { + "type": "object", + "properties": { + "Data_Type": { + "type": "string", + "enum": [ + "Database_Aggregated", + "Database_AI", + "Database_Full" + ] + }, + "Access_Method": { + "$ref": "#/components/schemas/Access_Method_Attribute" + }, + "Performance": { + "$ref": "#/components/schemas/DR_Performance_Database" + } + }, + "required": [ + "Data_Type", + "Performance" + ] + }, + { + "$ref": "#/components/schemas/Common_Extensions_Attributes" + } + ], + "unevaluatedProperties": false, + "x-tags": [ + "DR - Database Report" + ], + "title": "DR - Attribute_Performance Database", + "examples": [ + { + "Data_Type": "Database_Aggregated", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 47, + "2022-02": 59, + "2022-03": 38 + }, + "No_License": { + "2022-01": 41, + "2022-02": 77, + "2022-03": 42 + }, + "Searches_Automated": { + "2022-01": 15888, + "2022-02": 14802, + "2022-03": 15488 + }, + "Searches_Federated": { + "2022-01": 17178, + "2022-02": 17517, + "2022-03": 18637 + }, + "Searches_Regular": { + "2022-01": 13878, + "2022-02": 13332, + "2022-03": 19412 + } + } + } + ], + "x-stoplight": { + "id": "p078b5wzit4dd" + } + }, + "DR_Attribute_Performance_Other": { + "allOf": [ + { + "type": "object", + "properties": { + "Data_Type": { + "type": "string", + "enum": [ + "Audiovisual", + "Book", + "Conference", + "Database_Full_Item", + "Image", + "Interactive_Resource", + "Journal", + "Multimedia", + "Newspaper_or_Newsletter", + "Other", + "Patent", + "Reference_Work", + "Report", + "Sound", + "Standard", + "Thesis_or_Dissertation", + "Unspecified" + ] + }, + "Access_Method": { + "$ref": "#/components/schemas/Access_Method_Attribute" + }, + "Performance": { + "$ref": "#/components/schemas/DR_Performance_Other" + } + }, + "required": [ + "Data_Type", + "Performance" + ] + }, + { + "$ref": "#/components/schemas/Common_Extensions_Attributes" + } + ], + "unevaluatedProperties": false, + "x-tags": [ + "DR - Database Report" + ], + "title": "DR - Attribute_Performance Other", + "examples": [ + { + "Data_Type": "Book", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + } + ], + "x-stoplight": { + "id": "goh9fm0bl8fyd" + } + }, + "DR_Performance_Database": { + "title": "DR - Performance Database", + "examples": [ + { + "Limit_Exceeded": { + "2022-01": 47, + "2022-02": 59, + "2022-03": 38 + }, + "No_License": { + "2022-01": 41, + "2022-02": 77, + "2022-03": 42 + }, + "Searches_Automated": { + "2022-01": 15888, + "2022-02": 14802, + "2022-03": 15488 + }, + "Searches_Federated": { + "2022-01": 17178, + "2022-02": 17517, + "2022-03": 18637 + }, + "Searches_Regular": { + "2022-01": 13878, + "2022-02": 13332, + "2022-03": 19412 + } + } + ], + "type": "object", + "properties": { + "Searches_Automated": { + "$ref": "#/components/schemas/Counts" + }, + "Searches_Regular": { + "$ref": "#/components/schemas/Counts" + }, + "Searches_Federated": { + "$ref": "#/components/schemas/Counts" + }, + "Limit_Exceeded": { + "$ref": "#/components/schemas/Counts" + }, + "No_License": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 1, + "additionalProperties": false, + "x-stoplight": { + "id": "no6tr312wxemo" + }, + "x-tags": [ + "DR - Database Report" + ] + }, + "DR_Performance_Other": { + "title": "DR - Performance Other", + "examples": [ + { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + ], + "type": "object", + "properties": { + "Total_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Title_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Title_Requests": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-stoplight": { + "id": "x952chvd7cu98" + }, + "x-tags": [ + "DR - Database Report" + ] + }, + "DR_D1_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "DR_D1", + "type": "string" + }, + "Report_Name": { + "const": "Database Search and Item Usage", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/DR_D1_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "DR_D1 - Report_Header", + "x-stoplight": { + "id": "pswvaub5et9tk" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "DR_D1", + "Report_Name": "Database Search and Item Usage", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Searches_Automated", + "Searches_Federated", + "Searches_Regular", + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "DR_D1 - Database Search and Item Usage" + ] + }, + "DR_D1_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Metric_Type": { + "type": "array", + "minItems": 7, + "maxItems": 7, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Searches_Automated", + "Searches_Federated", + "Searches_Regular", + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests" + ] + } + } + }, + "required": [ + "Access_Method", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "DR_D1 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Searches_Automated", + "Searches_Federated", + "Searches_Regular", + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Access_Method": [ + "Regular" + ] + } + ], + "x-stoplight": { + "id": "9zfmg650jn92g" + }, + "x-tags": [ + "DR_D1 - Database Search and Item Usage" + ] + }, + "DR_D1_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Database_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/DR_D1_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "DR_D1 - Report_Item", + "x-stoplight": { + "id": "d0pdant0otxr1" + }, + "x-tags": [ + "DR_D1 - Database Search and Item Usage" + ], + "examples": [ + { + "Database": "Database 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB1" + }, + "Attribute_Performance": [ + { + "Performance": { + "Searches_Automated": { + "2022-01": 15888, + "2022-02": 14802, + "2022-03": 15488 + }, + "Searches_Federated": { + "2022-01": 17178, + "2022-02": 17517, + "2022-03": 18637 + }, + "Searches_Regular": { + "2022-01": 13878, + "2022-02": 13332, + "2022-03": 19412 + }, + "Total_Item_Investigations": { + "2022-01": 17546, + "2022-02": 18074, + "2022-03": 18284 + }, + "Total_Item_Requests": { + "2022-01": 10528, + "2022-02": 10845, + "2022-03": 10971 + }, + "Unique_Item_Investigations": { + "2022-01": 13162, + "2022-02": 13558, + "2022-03": 13714 + }, + "Unique_Item_Requests": { + "2022-01": 7897, + "2022-02": 8136, + "2022-03": 8229 + } + } + } + ] + } + ] + }, + "DR_D1_Attribute_Performance": { + "title": "DR_D1 - Attribute_Performance", + "examples": [ + { + "Performance": { + "Searches_Automated": { + "2022-01": 15888, + "2022-02": 14802, + "2022-03": 15488 + }, + "Searches_Federated": { + "2022-01": 17178, + "2022-02": 17517, + "2022-03": 18637 + }, + "Searches_Regular": { + "2022-01": 13878, + "2022-02": 13332, + "2022-03": 19412 + }, + "Total_Item_Investigations": { + "2022-01": 17546, + "2022-02": 18074, + "2022-03": 18284 + }, + "Total_Item_Requests": { + "2022-01": 10528, + "2022-02": 10845, + "2022-03": 10971 + }, + "Unique_Item_Investigations": { + "2022-01": 13162, + "2022-02": 13558, + "2022-03": 13714 + }, + "Unique_Item_Requests": { + "2022-01": 7897, + "2022-02": 8136, + "2022-03": 8229 + } + } + } + ], + "type": "object", + "properties": { + "Performance": { + "$ref": "#/components/schemas/DR_D1_Performance" + } + }, + "required": [ + "Performance" + ], + "additionalProperties": false, + "x-stoplight": { + "id": "nuo9p2td40827" + }, + "x-tags": [ + "DR_D1 - Database Search and Item Usage" + ] + }, + "DR_D1_Performance": { + "title": "DR_D1 - Performance", + "examples": [ + { + "Searches_Automated": { + "2022-01": 15888, + "2022-02": 14802, + "2022-03": 15488 + }, + "Searches_Federated": { + "2022-01": 17178, + "2022-02": 17517, + "2022-03": 18637 + }, + "Searches_Regular": { + "2022-01": 13878, + "2022-02": 13332, + "2022-03": 19412 + }, + "Total_Item_Investigations": { + "2022-01": 17546, + "2022-02": 18074, + "2022-03": 18284 + }, + "Total_Item_Requests": { + "2022-01": 10528, + "2022-02": 10845, + "2022-03": 10971 + }, + "Unique_Item_Investigations": { + "2022-01": 13162, + "2022-02": 13558, + "2022-03": 13714 + }, + "Unique_Item_Requests": { + "2022-01": 7897, + "2022-02": 8136, + "2022-03": 8229 + } + } + ], + "type": "object", + "properties": { + "Searches_Automated": { + "$ref": "#/components/schemas/Counts" + }, + "Searches_Regular": { + "$ref": "#/components/schemas/Counts" + }, + "Searches_Federated": { + "$ref": "#/components/schemas/Counts" + }, + "Total_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 1, + "additionalProperties": false, + "x-stoplight": { + "id": "fznuxhkp2on13" + }, + "x-tags": [ + "DR_D1 - Database Search and Item Usage" + ] + }, + "DR_D2_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "DR_D2", + "type": "string" + }, + "Report_Name": { + "const": "Database Access Denied", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/DR_D2_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "DR_D2 - Report_Header", + "x-stoplight": { + "id": "yxnsh846mw3bj" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "DR_D2", + "Report_Name": "Database Access Denied", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Limit_Exceeded", + "No_License" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "DR_D2 - Database Access Denied" + ] + }, + "DR_D2_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Metric_Type": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Limit_Exceeded", + "No_License" + ] + } + } + }, + "required": [ + "Access_Method", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "DR_D2 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Limit_Exceeded", + "No_License" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Access_Method": [ + "Regular" + ] + } + ], + "x-stoplight": { + "id": "ehemckrahnild" + }, + "x-tags": [ + "DR_D2 - Database Access Denied" + ] + }, + "DR_D2_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Database_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/DR_D2_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "DR_D2 - Report_Item", + "x-stoplight": { + "id": "hqb137oa5rxhl" + }, + "x-tags": [ + "DR_D2 - Database Access Denied" + ], + "examples": [ + { + "Database": "Database 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "Proprietary": "P1:DB1" + }, + "Attribute_Performance": [ + { + "Performance": { + "Limit_Exceeded": { + "2022-01": 47, + "2022-02": 59, + "2022-03": 38 + }, + "No_License": { + "2022-01": 41, + "2022-02": 77, + "2022-03": 42 + } + } + } + ] + } + ] + }, + "DR_D2_Attribute_Performance": { + "title": "DR_D2 - Attribute_Performance", + "examples": [ + { + "Performance": { + "Limit_Exceeded": { + "2022-01": 47, + "2022-02": 59, + "2022-03": 38 + }, + "No_License": { + "2022-01": 41, + "2022-02": 77, + "2022-03": 42 + } + } + } + ], + "type": "object", + "properties": { + "Performance": { + "$ref": "#/components/schemas/DR_D2_Performance" + } + }, + "required": [ + "Performance" + ], + "additionalProperties": false, + "x-stoplight": { + "id": "3e8xsgx6h4hyz" + }, + "x-tags": [ + "DR_D2 - Database Access Denied" + ] + }, + "DR_D2_Performance": { + "title": "DR_D2 - Performance", + "examples": [ + { + "Limit_Exceeded": { + "2022-01": 47, + "2022-02": 59, + "2022-03": 38 + }, + "No_License": { + "2022-01": 41, + "2022-02": 77, + "2022-03": 42 + } + } + ], + "type": "object", + "properties": { + "Limit_Exceeded": { + "$ref": "#/components/schemas/Counts" + }, + "No_License": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 1, + "additionalProperties": false, + "x-stoplight": { + "id": "1e4u5g7k6659n" + }, + "x-tags": [ + "DR_D2 - Database Access Denied" + ] + }, + "TR_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "TR", + "type": "string" + }, + "Report_Name": { + "const": "Title Report", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/TR_Report_Filters" + }, + "Report_Attributes": { + "$ref": "#/components/schemas/TR_Report_Attributes" + } + } + } + ], + "unevaluatedProperties": false, + "title": "TR - Report_Header", + "x-stoplight": { + "id": "4px3esieammx7" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "TR", + "Report_Name": "Title Report", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Attributes": { + "Attributes_To_Show": [ + "YOP", + "Access_Type", + "Access_Method" + ] + }, + "Report_Filters": { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + } + ], + "x-tags": [ + "TR - Title Report" + ] + }, + "TR_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Item_ID": { + "type": "string", + "minLength": 2 + }, + "Access_Method": { + "$ref": "#/components/schemas/Access_Method_Filter" + }, + "Access_Type": { + "$ref": "#/components/schemas/Access_Type_Filter" + }, + "Data_Type": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Book", + "Conference", + "Journal", + "Newspaper_or_Newsletter", + "Other", + "Patent", + "Reference_Work", + "Report", + "Standard", + "Thesis_or_Dissertation", + "Unspecified" + ] + } + }, + "Metric_Type": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests", + "Unique_Title_Investigations", + "Unique_Title_Requests", + "Limit_Exceeded", + "No_License" + ] + } + }, + "YOP": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^([0-9]{4}|[0-9]{4}-[0-9]{4})$" + } + } + } + }, + { + "$ref": "#/components/schemas/Common_Extensions_Filters" + } + ], + "unevaluatedProperties": false, + "title": "TR - Report_Filters", + "examples": [ + { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + ], + "x-stoplight": { + "id": "hqu7knig79f7x" + }, + "x-tags": [ + "TR - Title Report" + ] + }, + "TR_Report_Attributes": { + "allOf": [ + { + "type": "object", + "properties": { + "Attributes_To_Show": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "YOP", + "Access_Type", + "Access_Method", + "Institution_Name", + "Customer_ID", + "Country_Name", + "Country_Code", + "Subdivision_Name", + "Subdivision_Code", + "Attributed", + "Book_Segment_Count" + ] + } + } + } + }, + { + "$ref": "#/components/schemas/Granularity_Attribute" + } + ], + "minProperties": 1, + "unevaluatedProperties": false, + "title": "TR - Report_Attributes", + "examples": [ + { + "Attributes_To_Show": [ + "YOP", + "Access_Type", + "Access_Method" + ] + } + ], + "x-stoplight": { + "id": "w19a0wmpz6aj5" + }, + "x-tags": [ + "TR - Title Report" + ] + }, + "TR_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Title_Report_Item" + }, + { + "type": "object", + "properties": { + "Book_Segment_Count": { + "type": "integer", + "minimum": 1, + "description": "The number of Book_Segments within a Book. This is a common extension, support is optional (see Section 11 of the Code of Practice for details)." + } + } + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/TR_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR - Report_Item", + "x-stoplight": { + "id": "2j45lus8hc0t0" + }, + "x-tags": [ + "TR - Title Report" + ], + "examples": [ + { + "Title": "Title 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt01", + "Proprietary": "P1:T01", + "ISBN": "979-8-88888-888-8", + "URI": "https://doi.org/10.9999/xxxxt01" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 49, + "2022-02": 90, + "2022-03": 40 + }, + "No_License": { + "2022-01": 50, + "2022-02": 84, + "2022-03": 47 + }, + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + } + ] + } + ] + }, + "TR_Attribute_Performance": { + "allOf": [ + { + "type": "object", + "properties": { + "Data_Type": { + "type": "string", + "enum": [ + "Book", + "Conference", + "Journal", + "Newspaper_or_Newsletter", + "Other", + "Patent", + "Reference_Work", + "Report", + "Standard", + "Thesis_or_Dissertation", + "Unspecified" + ] + }, + "YOP": { + "type": "string", + "pattern": "^[0-9]{4}$" + }, + "Access_Type": { + "$ref": "#/components/schemas/Access_Type_Attribute" + }, + "Access_Method": { + "$ref": "#/components/schemas/Access_Method_Attribute" + }, + "Performance": { + "$ref": "#/components/schemas/TR_Performance" + } + }, + "required": [ + "Data_Type", + "Performance" + ] + }, + { + "$ref": "#/components/schemas/Common_Extensions_Attributes" + } + ], + "unevaluatedProperties": false, + "x-tags": [ + "TR - Title Report" + ], + "title": "TR - Attribute_Performance", + "examples": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 49, + "2022-02": 90, + "2022-03": 40 + }, + "No_License": { + "2022-01": 50, + "2022-02": 84, + "2022-03": 47 + }, + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + }, + { + "Data_Type": "Journal", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 87, + "2022-02": 88, + "2022-03": 45 + }, + "No_License": { + "2022-01": 48, + "2022-02": 80, + "2022-03": 37 + }, + "Total_Item_Investigations": { + "2022-01": 250, + "2022-02": 412, + "2022-03": 406 + }, + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Investigations": { + "2022-01": 188, + "2022-02": 309, + "2022-03": 304 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + } + ], + "x-stoplight": { + "id": "zlfhw0d3kxsj1" + } + }, + "TR_Performance": { + "title": "TR - Performance", + "examples": [ + { + "Limit_Exceeded": { + "2022-01": 49, + "2022-02": 90, + "2022-03": 40 + }, + "No_License": { + "2022-01": 50, + "2022-02": 84, + "2022-03": 47 + }, + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + ], + "type": "object", + "properties": { + "Total_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Title_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Title_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Limit_Exceeded": { + "$ref": "#/components/schemas/Counts" + }, + "No_License": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-stoplight": { + "id": "p5flk51bgxgp3" + }, + "x-tags": [ + "TR - Title Report" + ] + }, + "TR_B1_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "TR_B1", + "type": "string" + }, + "Report_Name": { + "const": "Book Requests (Controlled)", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/TR_B1_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "TR_B1 - Report_Header", + "x-stoplight": { + "id": "052s5ism0rvu2" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "TR_B1", + "Report_Name": "Book Requests (Controlled)", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Title_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Book", + "Reference_Work" + ], + "Access_Type": [ + "Controlled" + ], + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "TR_B1 - Book Requests (Controlled)" + ] + }, + "TR_B1_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Access_Type": { + "type": "array", + "const": [ + "Controlled" + ], + "items": { + "type": "string" + } + }, + "Data_Type": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Book", + "Reference_Work" + ] + } + }, + "Metric_Type": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Total_Item_Requests", + "Unique_Title_Requests" + ] + } + } + }, + "required": [ + "Access_Method", + "Access_Type", + "Data_Type", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_B1 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Title_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Book", + "Reference_Work" + ], + "Access_Type": [ + "Controlled" + ], + "Access_Method": [ + "Regular" + ] + } + ], + "x-stoplight": { + "id": "ht1u32e0qo3df" + }, + "x-tags": [ + "TR_B1 - Book Requests (Controlled)" + ] + }, + "TR_B1_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Title_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/TR_B1_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_B1 - Report_Item", + "x-stoplight": { + "id": "zz014q2jr5lzp" + }, + "x-tags": [ + "TR_B1 - Book Requests (Controlled)" + ], + "examples": [ + { + "Title": "Title 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt01", + "Proprietary": "P1:T01", + "ISBN": "979-8-88888-888-8", + "URI": "https://doi.org/10.9999/xxxxt01" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Performance": { + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + } + ] + } + ] + }, + "TR_B1_Attribute_Performance": { + "title": "TR_B1 - Attribute_Performance", + "examples": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Performance": { + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + } + ], + "type": "object", + "additionalProperties": false, + "properties": { + "Data_Type": { + "type": "string", + "enum": [ + "Book", + "Reference_Work" + ] + }, + "YOP": { + "type": "string", + "pattern": "^[0-9]{4}$" + }, + "Performance": { + "$ref": "#/components/schemas/TR_B1_Performance" + } + }, + "required": [ + "Data_Type", + "YOP", + "Performance" + ], + "x-stoplight": { + "id": "w48zc5xhfp5i4" + }, + "x-tags": [ + "TR_B1 - Book Requests (Controlled)" + ] + }, + "TR_B1_Performance": { + "title": "TR_B1 - Performance", + "examples": [ + { + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + ], + "type": "object", + "properties": { + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Title_Requests": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-stoplight": { + "id": "6vkszyzyrq9do" + }, + "x-tags": [ + "TR_B1 - Book Requests (Controlled)" + ] + }, + "TR_B2_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "TR_B2", + "type": "string" + }, + "Report_Name": { + "const": "Book Access Denied", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/TR_B2_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "TR_B2 - Report_Header", + "x-stoplight": { + "id": "v7s2f196gok2r" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "TR_B2", + "Report_Name": "Book Access Denied", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Limit_Exceeded", + "No_License" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Book", + "Reference_Work" + ], + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "TR_B2 - Book Access Denied" + ] + }, + "TR_B2_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Data_Type": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Book", + "Reference_Work" + ] + } + }, + "Metric_Type": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Limit_Exceeded", + "No_License" + ] + } + } + }, + "required": [ + "Access_Method", + "Data_Type", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_B2 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Limit_Exceeded", + "No_License" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Book", + "Reference_Work" + ], + "Access_Method": [ + "Regular" + ] + } + ], + "x-stoplight": { + "id": "phn2zdtzdjf2m" + }, + "x-tags": [ + "TR_B2 - Book Access Denied" + ] + }, + "TR_B2_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Title_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/TR_B2_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_B2 - Report_Item", + "x-stoplight": { + "id": "2jo2bto0i0de6" + }, + "x-tags": [ + "TR_B2 - Book Access Denied" + ], + "examples": [ + { + "Title": "Title 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt01", + "Proprietary": "P1:T01", + "ISBN": "979-8-88888-888-8", + "URI": "https://doi.org/10.9999/xxxxt01" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Performance": { + "Limit_Exceeded": { + "2022-01": 49, + "2022-02": 90, + "2022-03": 40 + }, + "No_License": { + "2022-01": 50, + "2022-02": 84, + "2022-03": 47 + } + } + } + ] + } + ] + }, + "TR_B2_Attribute_Performance": { + "title": "TR_B2 - Attribute_Performance", + "examples": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Performance": { + "Limit_Exceeded": { + "2022-01": 49, + "2022-02": 90, + "2022-03": 40 + }, + "No_License": { + "2022-01": 50, + "2022-02": 84, + "2022-03": 47 + } + } + } + ], + "type": "object", + "additionalProperties": false, + "properties": { + "Data_Type": { + "type": "string", + "enum": [ + "Book", + "Reference_Work" + ] + }, + "YOP": { + "type": "string", + "pattern": "^[0-9]{4}$" + }, + "Performance": { + "$ref": "#/components/schemas/TR_B2_Performance" + } + }, + "required": [ + "Data_Type", + "YOP", + "Performance" + ], + "x-stoplight": { + "id": "qdlqgri5gl52e" + }, + "x-tags": [ + "TR_B2 - Book Access Denied" + ] + }, + "TR_B2_Performance": { + "title": "TR_B2 - Performance", + "examples": [ + { + "Limit_Exceeded": { + "2022-01": 49, + "2022-02": 90, + "2022-03": 40 + }, + "No_License": { + "2022-01": 50, + "2022-02": 84, + "2022-03": 47 + } + } + ], + "type": "object", + "properties": { + "Limit_Exceeded": { + "$ref": "#/components/schemas/Counts" + }, + "No_License": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-stoplight": { + "id": "zj5935hqgiuid" + }, + "x-tags": [ + "TR_B2 - Book Access Denied" + ] + }, + "TR_B3_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "TR_B3", + "type": "string" + }, + "Report_Name": { + "const": "Book Usage by Access Type", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/TR_B3_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "TR_B3 - Report_Header", + "x-stoplight": { + "id": "gerptpja96rhf" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "TR_B3", + "Report_Name": "Book Usage by Access Type", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests", + "Unique_Title_Investigations", + "Unique_Title_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Book", + "Reference_Work" + ], + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "TR_B3 - Book Usage by Access Type" + ] + }, + "TR_B3_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Data_Type": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Book", + "Reference_Work" + ] + } + }, + "Metric_Type": { + "type": "array", + "minItems": 6, + "maxItems": 6, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests", + "Unique_Title_Investigations", + "Unique_Title_Requests" + ] + } + } + }, + "required": [ + "Access_Method", + "Data_Type", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_B3 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests", + "Unique_Title_Investigations", + "Unique_Title_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Book", + "Reference_Work" + ], + "Access_Method": [ + "Regular" + ] + } + ], + "x-stoplight": { + "id": "ke9h8rxb7r0bg" + }, + "x-tags": [ + "TR_B3 - Book Usage by Access Type" + ] + }, + "TR_B3_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Title_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/TR_B3_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_B3 - Report_Item", + "x-stoplight": { + "id": "6wec4mmh74hco" + }, + "x-tags": [ + "TR_B3 - Book Usage by Access Type" + ], + "examples": [ + { + "Title": "Title 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt01", + "Proprietary": "P1:T01", + "ISBN": "979-8-88888-888-8", + "URI": "https://doi.org/10.9999/xxxxt01" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Access_Type": "Controlled", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + } + ] + } + ] + }, + "TR_B3_Attribute_Performance": { + "title": "TR_B3 - Attribute_Performance", + "examples": [ + { + "Data_Type": "Book", + "YOP": "2022", + "Access_Type": "Controlled", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + } + ], + "type": "object", + "additionalProperties": false, + "properties": { + "Data_Type": { + "type": "string", + "enum": [ + "Book", + "Reference_Work" + ] + }, + "YOP": { + "type": "string", + "pattern": "^[0-9]{4}$" + }, + "Access_Type": { + "$ref": "#/components/schemas/Access_Type_Attribute" + }, + "Performance": { + "$ref": "#/components/schemas/TR_B3_Performance" + } + }, + "required": [ + "Data_Type", + "YOP", + "Access_Type", + "Performance" + ], + "x-stoplight": { + "id": "o4jcchw7spi1m" + }, + "x-tags": [ + "TR_B3 - Book Usage by Access Type" + ] + }, + "TR_B3_Performance": { + "title": "TR_B3 - Performance", + "examples": [ + { + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + }, + "Unique_Title_Investigations": { + "2022-01": 552, + "2022-02": 623, + "2022-03": 665 + }, + "Unique_Title_Requests": { + "2022-01": 414, + "2022-02": 467, + "2022-03": 499 + } + } + ], + "type": "object", + "properties": { + "Total_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Title_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Title_Requests": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-stoplight": { + "id": "v8lpogu3luk9f" + }, + "x-tags": [ + "TR_B3 - Book Usage by Access Type" + ] + }, + "TR_J1_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "TR_J1", + "type": "string" + }, + "Report_Name": { + "const": "Journal Requests (Controlled)", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/TR_J1_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "TR_J1 - Report_Header", + "x-stoplight": { + "id": "ne5z08rqav4gw" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "TR_J1", + "Report_Name": "Journal Requests (Controlled)", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Type": [ + "Controlled" + ], + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "TR_J1 - Journal Requests (Controlled)" + ] + }, + "TR_J1_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Access_Type": { + "type": "array", + "const": [ + "Controlled" + ], + "items": { + "type": "string" + } + }, + "Data_Type": { + "type": "array", + "const": [ + "Journal" + ], + "items": { + "type": "string" + } + }, + "Metric_Type": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ] + } + } + }, + "required": [ + "Access_Method", + "Access_Type", + "Data_Type", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_J1 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Type": [ + "Controlled" + ], + "Access_Method": [ + "Regular" + ] + } + ], + "x-stoplight": { + "id": "oc13fhmyf0nyb" + }, + "x-tags": [ + "TR_J1 - Journal Requests (Controlled)" + ] + }, + "TR_J1_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Title_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/TR_J1_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_J1 - Report_Item", + "x-stoplight": { + "id": "ktwpp8cihnoto" + }, + "x-tags": [ + "TR_J1 - Journal Requests (Controlled)" + ], + "examples": [ + { + "Title": "Title 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Attribute_Performance": [ + { + "Performance": { + "Total_Item_Requests": { + "2022-01": 357, + "2022-02": 551, + "2022-03": 543 + }, + "Unique_Item_Requests": { + "2022-01": 283, + "2022-02": 429, + "2022-03": 423 + } + } + } + ] + } + ] + }, + "TR_J1_Attribute_Performance": { + "title": "TR_J1 - Attribute_Performance", + "examples": [ + { + "Performance": { + "Total_Item_Requests": { + "2022-01": 357, + "2022-02": 551, + "2022-03": 543 + }, + "Unique_Item_Requests": { + "2022-01": 283, + "2022-02": 429, + "2022-03": 423 + } + } + } + ], + "type": "object", + "additionalProperties": false, + "properties": { + "Performance": { + "$ref": "#/components/schemas/TR_J1_Performance" + } + }, + "required": [ + "Performance" + ], + "x-stoplight": { + "id": "zztkqu02imqrn" + }, + "x-tags": [ + "TR_J1 - Journal Requests (Controlled)" + ] + }, + "TR_J1_Performance": { + "title": "TR_J1 - Performance", + "examples": [ + { + "Total_Item_Requests": { + "2022-01": 357, + "2022-02": 551, + "2022-03": 543 + }, + "Unique_Item_Requests": { + "2022-01": 283, + "2022-02": 429, + "2022-03": 423 + } + } + ], + "type": "object", + "properties": { + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-stoplight": { + "id": "v8rojqhw01j0d" + }, + "x-tags": [ + "TR_J1 - Journal Requests (Controlled)" + ] + }, + "TR_J2_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "TR_J2", + "type": "string" + }, + "Report_Name": { + "const": "Journal Access Denied", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/TR_J2_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "TR_J2 - Report_Header", + "x-stoplight": { + "id": "xx7mq667z673g" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "TR_J2", + "Report_Name": "Journal Access Denied", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Limit_Exceeded", + "No_License" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "TR_J2 - Journal Access Denied" + ] + }, + "TR_J2_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Data_Type": { + "type": "array", + "const": [ + "Journal" + ], + "items": { + "type": "string" + } + }, + "Metric_Type": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Limit_Exceeded", + "No_License" + ] + } + } + }, + "required": [ + "Access_Method", + "Data_Type", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_J2 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Limit_Exceeded", + "No_License" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Method": [ + "Regular" + ] + } + ], + "x-stoplight": { + "id": "c53mkv2xtx80f" + }, + "x-tags": [ + "TR_J2 - Journal Access Denied" + ] + }, + "TR_J2_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Title_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/TR_J2_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_J2 - Report_Item", + "x-stoplight": { + "id": "0q1tqzfy456xc" + }, + "x-tags": [ + "TR_J2 - Journal Access Denied" + ], + "examples": [ + { + "Title": "Title 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Attribute_Performance": [ + { + "Performance": { + "Limit_Exceeded": { + "2022-01": 231, + "2022-02": 233, + "2022-03": 147 + }, + "No_License": { + "2022-01": 153, + "2022-02": 217, + "2022-03": 131 + } + } + } + ] + } + ] + }, + "TR_J2_Attribute_Performance": { + "title": "TR_J2 - Attribute_Performance", + "examples": [ + { + "Performance": { + "Limit_Exceeded": { + "2022-01": 231, + "2022-02": 233, + "2022-03": 147 + }, + "No_License": { + "2022-01": 153, + "2022-02": 217, + "2022-03": 131 + } + } + } + ], + "type": "object", + "additionalProperties": false, + "properties": { + "Performance": { + "$ref": "#/components/schemas/TR_J2_Performance" + } + }, + "required": [ + "Performance" + ], + "x-stoplight": { + "id": "2emywscrz0brs" + }, + "x-tags": [ + "TR_J2 - Journal Access Denied" + ] + }, + "TR_J2_Performance": { + "title": "TR_J2 - Performance", + "examples": [ + { + "Limit_Exceeded": { + "2022-01": 231, + "2022-02": 233, + "2022-03": 147 + }, + "No_License": { + "2022-01": 153, + "2022-02": 217, + "2022-03": 131 + } + } + ], + "type": "object", + "properties": { + "Limit_Exceeded": { + "$ref": "#/components/schemas/Counts" + }, + "No_License": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 1, + "additionalProperties": false, + "x-stoplight": { + "id": "x8qo2u5phttqj" + }, + "x-tags": [ + "TR_J2 - Journal Access Denied" + ] + }, + "TR_J3_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "TR_J3", + "type": "string" + }, + "Report_Name": { + "const": "Journal Usage by Access Type", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/TR_J3_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "TR_J3 - Report_Header", + "x-stoplight": { + "id": "ej0zm339opbvi" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "TR_J3", + "Report_Name": "Journal Usage by Access Type", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "TR_J3 - Journal Usage by Access Type" + ] + }, + "TR_J3_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Data_Type": { + "type": "array", + "const": [ + "Journal" + ], + "items": { + "type": "string" + } + }, + "Metric_Type": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests" + ] + } + } + }, + "required": [ + "Access_Method", + "Data_Type", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_J3 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Method": [ + "Regular" + ] + } + ], + "x-stoplight": { + "id": "eugdonlrs30ln" + }, + "x-tags": [ + "TR_J3 - Journal Usage by Access Type" + ] + }, + "TR_J3_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Title_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/TR_J3_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_J3 - Report_Item", + "x-stoplight": { + "id": "fysmk0zj37qw5" + }, + "x-tags": [ + "TR_J3 - Journal Usage by Access Type" + ], + "examples": [ + { + "Title": "Title 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Attribute_Performance": [ + { + "Access_Type": "Controlled", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 557, + "2022-02": 881, + "2022-03": 869 + }, + "Total_Item_Requests": { + "2022-01": 357, + "2022-02": 551, + "2022-03": 543 + }, + "Unique_Item_Investigations": { + "2022-01": 433, + "2022-02": 675, + "2022-03": 665 + }, + "Unique_Item_Requests": { + "2022-01": 283, + "2022-02": 429, + "2022-03": 423 + } + } + }, + { + "Access_Type": "Open", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1565, + "2022-02": 2537, + "2022-03": 2495 + }, + "Total_Item_Requests": { + "2022-01": 963, + "2022-02": 1547, + "2022-03": 1523 + }, + "Unique_Item_Investigations": { + "2022-01": 1189, + "2022-02": 1919, + "2022-03": 1887 + }, + "Unique_Item_Requests": { + "2022-01": 737, + "2022-02": 1175, + "2022-03": 1157 + } + } + } + ] + } + ] + }, + "TR_J3_Attribute_Performance": { + "title": "TR_J3 - Attribute_Performance", + "examples": [ + { + "Access_Type": "Controlled", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 557, + "2022-02": 881, + "2022-03": 869 + }, + "Total_Item_Requests": { + "2022-01": 357, + "2022-02": 551, + "2022-03": 543 + }, + "Unique_Item_Investigations": { + "2022-01": 433, + "2022-02": 675, + "2022-03": 665 + }, + "Unique_Item_Requests": { + "2022-01": 283, + "2022-02": 429, + "2022-03": 423 + } + } + } + ], + "type": "object", + "additionalProperties": false, + "properties": { + "Access_Type": { + "$ref": "#/components/schemas/Access_Type_Attribute" + }, + "Performance": { + "$ref": "#/components/schemas/TR_J3_Performance" + } + }, + "required": [ + "Access_Type", + "Performance" + ], + "x-stoplight": { + "id": "v0in3ypw4e1ha" + }, + "x-tags": [ + "TR_J3 - Journal Usage by Access Type" + ] + }, + "TR_J3_Performance": { + "title": "TR_J3 - Performance", + "examples": [ + { + "Total_Item_Investigations": { + "2022-01": 557, + "2022-02": 881, + "2022-03": 869 + }, + "Total_Item_Requests": { + "2022-01": 357, + "2022-02": 551, + "2022-03": 543 + }, + "Unique_Item_Investigations": { + "2022-01": 433, + "2022-02": 675, + "2022-03": 665 + }, + "Unique_Item_Requests": { + "2022-01": 283, + "2022-02": 429, + "2022-03": 423 + } + } + ], + "type": "object", + "properties": { + "Total_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-stoplight": { + "id": "5wmivrzedovb9" + }, + "x-tags": [ + "TR_J3 - Journal Usage by Access Type" + ] + }, + "TR_J4_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "TR_J4", + "type": "string" + }, + "Report_Name": { + "const": "Journal Requests by YOP (Controlled)", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/TR_J4_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "TR_J4 - Report_Header", + "x-stoplight": { + "id": "7s9bgnevaxwr4" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "TR_J4", + "Report_Name": "Journal Requests by YOP (Controlled)", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Type": [ + "Controlled" + ], + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "TR_J4 - Journal Requests by YOP (Controlled)" + ] + }, + "TR_J4_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Access_Type": { + "type": "array", + "const": [ + "Controlled" + ], + "items": { + "type": "string" + } + }, + "Data_Type": { + "type": "array", + "const": [ + "Journal" + ], + "items": { + "type": "string" + } + }, + "Metric_Type": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ] + } + } + }, + "required": [ + "Access_Method", + "Access_Type", + "Data_Type", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_J4 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Journal" + ], + "Access_Type": [ + "Controlled" + ], + "Access_Method": [ + "Regular" + ] + } + ], + "x-stoplight": { + "id": "uydreg86r0oox" + }, + "x-tags": [ + "TR_J4 - Journal Requests by YOP (Controlled)" + ] + }, + "TR_J4_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Title_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/TR_J4_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "TR_J4 - Report_Item", + "x-stoplight": { + "id": "0hhok4efh8q6x" + }, + "x-tags": [ + "TR_J4 - Journal Requests by YOP (Controlled)" + ], + "examples": [ + { + "Title": "Title 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Attribute_Performance": [ + { + "YOP": "2022", + "Performance": { + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + }, + { + "YOP": "2021", + "Performance": { + "Total_Item_Requests": { + "2022-01": 207, + "2022-02": 304, + "2022-03": 300 + }, + "Unique_Item_Requests": { + "2022-01": 170, + "2022-02": 243, + "2022-03": 240 + } + } + } + ] + } + ] + }, + "TR_J4_Attribute_Performance": { + "title": "TR_J4 - Attribute_Performance", + "examples": [ + { + "YOP": "2022", + "Performance": { + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + } + ], + "type": "object", + "additionalProperties": false, + "properties": { + "YOP": { + "type": "string", + "pattern": "^[0-9]{4}$" + }, + "Performance": { + "$ref": "#/components/schemas/TR_J4_Performance" + } + }, + "required": [ + "YOP", + "Performance" + ], + "x-stoplight": { + "id": "nahfdpzghvbe1" + }, + "x-tags": [ + "TR_J4 - Journal Requests by YOP (Controlled)" + ] + }, + "TR_J4_Performance": { + "title": "TR_J4 - Performance", + "examples": [ + { + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + ], + "type": "object", + "properties": { + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-stoplight": { + "id": "m8hph9z49etcf" + }, + "x-tags": [ + "TR_J4 - Journal Requests by YOP (Controlled)" + ] + }, + "IR_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "IR", + "type": "string" + }, + "Report_Name": { + "const": "Item Report", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/IR_Report_Filters" + }, + "Report_Attributes": { + "$ref": "#/components/schemas/IR_Report_Attributes" + } + } + } + ], + "unevaluatedProperties": false, + "title": "IR - Report_Header", + "x-stoplight": { + "id": "f95x45jjypkso" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "IR", + "Report_Name": "Item Report", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Attributes": { + "Attributes_To_Show": [ + "Authors", + "Publication_Date", + "Article_Version", + "YOP", + "Access_Type", + "Access_Method" + ], + "Include_Parent_Details": "True" + }, + "Report_Filters": { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + } + ], + "x-tags": [ + "IR - Item Report" + ] + }, + "IR_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Author": { + "type": "string", + "minLength": 2 + }, + "Item_ID": { + "type": "string", + "minLength": 2 + }, + "Access_Method": { + "$ref": "#/components/schemas/Access_Method_Filter" + }, + "Access_Type": { + "$ref": "#/components/schemas/Access_Type_Filter" + }, + "Data_Type": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Article", + "Audiovisual", + "Book_Segment", + "Conference_Item", + "Database_Full_Item", + "Dataset", + "Image", + "Interactive_Resource", + "Multimedia", + "News_Item", + "Other", + "Patent", + "Reference_Item", + "Report", + "Software", + "Sound", + "Standard", + "Thesis_or_Dissertation", + "Unspecified" + ] + } + }, + "Metric_Type": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Total_Item_Investigations", + "Total_Item_Requests", + "Unique_Item_Investigations", + "Unique_Item_Requests", + "Limit_Exceeded", + "No_License" + ] + } + }, + "YOP": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^([0-9]{4}|[0-9]{4}-[0-9]{4})$" + } + } + } + }, + { + "$ref": "#/components/schemas/Common_Extensions_Filters" + } + ], + "unevaluatedProperties": false, + "title": "IR - Report_Filters", + "examples": [ + { + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31" + } + ], + "x-stoplight": { + "id": "hbvg6q85px0i4" + }, + "x-tags": [ + "IR - Item Report" + ] + }, + "IR_Report_Attributes": { + "allOf": [ + { + "type": "object", + "properties": { + "Attributes_To_Show": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "YOP", + "Access_Type", + "Access_Method", + "Authors", + "Publication_Date", + "Article_Version", + "Institution_Name", + "Customer_ID", + "Country_Name", + "Country_Code", + "Subdivision_Name", + "Subdivision_Code", + "Attributed" + ] + } + }, + "Include_Parent_Details": { + "type": "string", + "enum": [ + "False", + "True" + ] + }, + "Include_Component_Details": { + "type": "string", + "enum": [ + "False", + "True" + ] + } + } + }, + { + "$ref": "#/components/schemas/Granularity_Attribute" + } + ], + "minProperties": 1, + "unevaluatedProperties": false, + "title": "IR - Report_Attributes", + "examples": [ + { + "Attributes_To_Show": [ + "Authors", + "Publication_Date", + "Article_Version", + "YOP", + "Access_Type", + "Access_Method" + ], + "Include_Parent_Details": "True" + } + ], + "x-stoplight": { + "id": "ohqp080zkfexh" + }, + "x-tags": [ + "IR - Item Report" + ] + }, + "IR_Parent_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Item_Parent_Item" + }, + { + "type": "object", + "properties": { + "Items": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/IR_Report_Item" + } + } + }, + "required": [ + "Items" + ] + } + ], + "unevaluatedProperties": false, + "title": "IR - Parent_Item", + "x-stoplight": { + "id": "vpv4va9egpoue" + }, + "x-tags": [ + "IR - Item Report" + ], + "examples": [ + { + "Title": "Title 1", + "Data_Type": "Book", + "Item_ID": { + "DOI": "10.9999/xxxxt01", + "Proprietary": "P1:T01", + "ISBN": "979-8-88888-888-8", + "URI": "https://doi.org/10.9999/xxxxt01" + }, + "Items": [ + { + "Item": "Item 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 3" + } + ], + "Publication_Date": "2022-07-24", + "Item_ID": { + "DOI": "10.9999/xxxxi03", + "Proprietary": "P1:I03", + "URI": "https://doi.org/10.9999/xxxxi03" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book_Segment", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 49, + "2022-02": 90, + "2022-03": 40 + }, + "No_License": { + "2022-01": 50, + "2022-02": 84, + "2022-03": 47 + }, + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + } + } + } + ] + } + ] + } + ] + }, + "IR_No_Parent_Item": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/IR_Report_Item" + } + } + }, + "required": [ + "Items" + ], + "title": "IR - No Parent_Item", + "additionalProperties": false, + "x-stoplight": { + "id": "m696f8340w7k2" + }, + "x-tags": [ + "IR - Item Report" + ], + "examples": [ + { + "Items": [ + { + "Item": "Item 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 1" + } + ], + "Publication_Date": "2022-07-19", + "Item_ID": { + "DOI": "10.9999/xxxxi01", + "Proprietary": "P1:I01", + "URI": "https://doi.org/10.9999/xxxxi01" + }, + "Attribute_Performance": [ + { + "Data_Type": "Article", + "YOP": "2022", + "Access_Type": "Open", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1461, + "2022-02": 1704, + "2022-03": 1475 + }, + "Total_Item_Requests": { + "2022-01": 877, + "2022-02": 1022, + "2022-03": 885 + }, + "Unique_Item_Investigations": { + "2022-01": 1096, + "2022-02": 1278, + "2022-03": 1106 + }, + "Unique_Item_Requests": { + "2022-01": 658, + "2022-02": 767, + "2022-03": 664 + } + } + } + ] + }, + { + "Item": "Item 4", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 4" + } + ], + "Publication_Date": "2019-08-17", + "Item_ID": { + "DOI": "10.9999/xxxxi04", + "Proprietary": "P1:I04", + "URI": "https://doi.org/10.9999/xxxxi04" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book_Segment", + "YOP": "2019", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 50, + "2022-02": 43, + "2022-03": 68 + }, + "No_License": { + "2022-01": 33, + "2022-02": 68, + "2022-03": 58 + }, + "Total_Item_Investigations": { + "2022-01": 1156, + "2022-02": 1289, + "2022-03": 1821 + }, + "Total_Item_Requests": { + "2022-01": 694, + "2022-02": 773, + "2022-03": 1093 + }, + "Unique_Item_Investigations": { + "2022-01": 867, + "2022-02": 967, + "2022-03": 1366 + }, + "Unique_Item_Requests": { + "2022-01": 521, + "2022-02": 580, + "2022-03": 820 + } + } + } + ] + }, + { + "Item": "Item 6", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 6" + } + ], + "Publication_Date": "2020-08-09", + "Item_ID": { + "DOI": "10.9999/xxxxi06", + "Proprietary": "P1:I06", + "URI": "https://doi.org/10.9999/xxxxi06" + }, + "Attribute_Performance": [ + { + "Data_Type": "Conference_Item", + "YOP": "2020", + "Access_Type": "Free_To_Read", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1877, + "2022-02": 1261, + "2022-03": 1335 + }, + "Total_Item_Requests": { + "2022-01": 1126, + "2022-02": 757, + "2022-03": 801 + }, + "Unique_Item_Investigations": { + "2022-01": 1408, + "2022-02": 946, + "2022-03": 1001 + }, + "Unique_Item_Requests": { + "2022-01": 845, + "2022-02": 568, + "2022-03": 601 + } + } + } + ] + }, + { + "Item": "Item 8", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 8" + } + ], + "Publication_Date": "2020-09-10", + "Item_ID": { + "DOI": "10.9999/xxxxi08", + "Proprietary": "P1:I08", + "URI": "https://doi.org/10.9999/xxxxi08" + }, + "Attribute_Performance": [ + { + "Data_Type": "Dataset", + "YOP": "2020", + "Access_Type": "Open", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1329, + "2022-02": 1501, + "2022-03": 1276 + }, + "Total_Item_Requests": { + "2022-01": 797, + "2022-02": 901, + "2022-03": 766 + }, + "Unique_Item_Investigations": { + "2022-01": 997, + "2022-02": 1126, + "2022-03": 957 + }, + "Unique_Item_Requests": { + "2022-01": 598, + "2022-02": 676, + "2022-03": 575 + } + } + } + ] + }, + { + "Item": "Item 14", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 14" + } + ], + "Publication_Date": "2018-01-13", + "Item_ID": { + "DOI": "10.9999/xxxxi14", + "Proprietary": "P1:I14", + "URI": "https://doi.org/10.9999/xxxxi14" + }, + "Attribute_Performance": [ + { + "Data_Type": "News_Item", + "YOP": "2018", + "Access_Type": "Free_To_Read", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1687, + "2022-02": 1732, + "2022-03": 1025 + }, + "Total_Item_Requests": { + "2022-01": 1012, + "2022-02": 1039, + "2022-03": 615 + }, + "Unique_Item_Investigations": { + "2022-01": 1265, + "2022-02": 1299, + "2022-03": 769 + }, + "Unique_Item_Requests": { + "2022-01": 759, + "2022-02": 779, + "2022-03": 461 + } + } + } + ] + }, + { + "Item": "Item 16", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 16" + } + ], + "Publication_Date": "2019-11-01", + "Item_ID": { + "DOI": "10.9999/xxxxi16", + "Proprietary": "P1:I16", + "URI": "https://doi.org/10.9999/xxxxi16" + }, + "Attribute_Performance": [ + { + "Data_Type": "Other", + "YOP": "2019", + "Access_Type": "Free_To_Read", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1730, + "2022-02": 1263, + "2022-03": 1787 + }, + "Total_Item_Requests": { + "2022-01": 1038, + "2022-02": 758, + "2022-03": 1072 + }, + "Unique_Item_Investigations": { + "2022-01": 1298, + "2022-02": 947, + "2022-03": 1340 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 569, + "2022-03": 804 + } + } + } + ] + }, + { + "Item": "Item 18", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 18" + } + ], + "Publication_Date": "2021-09-03", + "Item_ID": { + "DOI": "10.9999/xxxxi18", + "Proprietary": "P1:I18", + "URI": "https://doi.org/10.9999/xxxxi18" + }, + "Attribute_Performance": [ + { + "Data_Type": "Reference_Item", + "YOP": "2021", + "Access_Type": "Open", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1546, + "2022-02": 1408, + "2022-03": 1204 + }, + "Total_Item_Requests": { + "2022-01": 928, + "2022-02": 845, + "2022-03": 722 + }, + "Unique_Item_Investigations": { + "2022-01": 1160, + "2022-02": 1056, + "2022-03": 903 + }, + "Unique_Item_Requests": { + "2022-01": 696, + "2022-02": 634, + "2022-03": 542 + } + } + } + ] + }, + { + "Item": "Item 21", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 21" + } + ], + "Publication_Date": "2020-02-08", + "Item_ID": { + "DOI": "10.9999/xxxxi21", + "Proprietary": "P1:I21", + "URI": "https://doi.org/10.9999/xxxxi21" + }, + "Attribute_Performance": [ + { + "Data_Type": "Software", + "YOP": "2020", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 43, + "2022-02": 59, + "2022-03": 38 + }, + "No_License": { + "2022-01": 31, + "2022-02": 44, + "2022-03": 61 + }, + "Total_Item_Investigations": { + "2022-01": 1187, + "2022-02": 1775, + "2022-03": 1416 + }, + "Total_Item_Requests": { + "2022-01": 712, + "2022-02": 1065, + "2022-03": 850 + }, + "Unique_Item_Investigations": { + "2022-01": 890, + "2022-02": 1331, + "2022-03": 1062 + }, + "Unique_Item_Requests": { + "2022-01": 534, + "2022-02": 799, + "2022-03": 638 + } + } + } + ] + } + ] + } + ] + }, + "IR_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Item_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/IR_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "IR - Report_Item", + "x-stoplight": { + "id": "t1hr7f6s0ipyl" + }, + "x-tags": [ + "IR - Item Report" + ], + "examples": [ + { + "Item": "Item 3", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 3" + } + ], + "Publication_Date": "2022-07-24", + "Item_ID": { + "DOI": "10.9999/xxxxi03", + "Proprietary": "P1:I03", + "URI": "https://doi.org/10.9999/xxxxi03" + }, + "Attribute_Performance": [ + { + "Data_Type": "Book_Segment", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 49, + "2022-02": 90, + "2022-03": 40 + }, + "No_License": { + "2022-01": 50, + "2022-02": 84, + "2022-03": 47 + }, + "Total_Item_Investigations": { + "2022-01": 1104, + "2022-02": 1246, + "2022-03": 1329 + }, + "Total_Item_Requests": { + "2022-01": 662, + "2022-02": 748, + "2022-03": 797 + }, + "Unique_Item_Investigations": { + "2022-01": 828, + "2022-02": 935, + "2022-03": 997 + }, + "Unique_Item_Requests": { + "2022-01": 497, + "2022-02": 561, + "2022-03": 598 + } + } + } + ] + } + ] + }, + "IR_Attribute_Performance": { + "allOf": [ + { + "type": "object", + "properties": { + "Data_Type": { + "type": "string", + "enum": [ + "Article", + "Audiovisual", + "Book_Segment", + "Conference_Item", + "Database_Full_Item", + "Dataset", + "Image", + "Interactive_Resource", + "Multimedia", + "News_Item", + "Other", + "Patent", + "Reference_Item", + "Report", + "Software", + "Sound", + "Standard", + "Thesis_or_Dissertation", + "Unspecified" + ] + }, + "YOP": { + "type": "string", + "pattern": "^[0-9]{4}$" + }, + "Access_Type": { + "$ref": "#/components/schemas/Access_Type_Attribute" + }, + "Access_Method": { + "$ref": "#/components/schemas/Access_Method_Attribute" + }, + "Performance": { + "$ref": "#/components/schemas/IR_Performance" + }, + "Components": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IR_Component_Item" + }, + "uniqueItems": true, + "minItems": 1 + } + }, + "required": [ + "Data_Type", + "Performance" + ] + }, + { + "$ref": "#/components/schemas/Common_Extensions_Attributes" + } + ], + "unevaluatedProperties": false, + "x-tags": [ + "IR - Item Report" + ], + "title": "IR - Attribute_Performance", + "examples": [ + { + "Data_Type": "Article", + "YOP": "2022", + "Access_Type": "Controlled", + "Access_Method": "Regular", + "Performance": { + "Limit_Exceeded": { + "2022-01": 87, + "2022-02": 88, + "2022-03": 45 + }, + "No_License": { + "2022-01": 48, + "2022-02": 80, + "2022-03": 37 + }, + "Total_Item_Investigations": { + "2022-01": 250, + "2022-02": 412, + "2022-03": 406 + }, + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Investigations": { + "2022-01": 188, + "2022-02": 309, + "2022-03": 304 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + }, + { + "Data_Type": "Multimedia", + "YOP": "2020", + "Access_Type": "Free_To_Read", + "Access_Method": "Regular", + "Performance": { + "Total_Item_Investigations": { + "2022-01": 1865, + "2022-02": 1891, + "2022-03": 1707 + }, + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Investigations": { + "2022-01": 1399, + "2022-02": 1418, + "2022-03": 1280 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + } + ], + "x-stoplight": { + "id": "1l145u6dnc16m" + } + }, + "IR_Performance": { + "title": "IR - Performance", + "examples": [ + { + "Limit_Exceeded": { + "2022-01": 87, + "2022-02": 88, + "2022-03": 45 + }, + "No_License": { + "2022-01": 48, + "2022-02": 80, + "2022-03": 37 + }, + "Total_Item_Investigations": { + "2022-01": 250, + "2022-02": 412, + "2022-03": 406 + }, + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Investigations": { + "2022-01": 188, + "2022-02": 309, + "2022-03": 304 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + ], + "type": "object", + "properties": { + "Total_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Limit_Exceeded": { + "$ref": "#/components/schemas/Counts" + }, + "No_License": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 1, + "unevaluatedProperties": false, + "x-stoplight": { + "id": "b9daqz8v107qm" + }, + "x-tags": [ + "IR - Item Report" + ] + }, + "IR_Component_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Item_Component_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/IR_Component_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "IR - Component_Item", + "x-stoplight": { + "id": "dvooh7y4lljs2" + }, + "x-tags": [ + "IR - Item Report" + ] + }, + "IR_Component_Attribute_Performance": { + "title": "IR - Component Attribute_Performance", + "type": "object", + "additionalProperties": false, + "properties": { + "Data_Type": { + "type": "string", + "enum": [ + "Article", + "Audiovisual", + "Book_Segment", + "Conference_Item", + "Database_Full_Item", + "Dataset", + "Image", + "Interactive_Resource", + "Multimedia", + "News_Item", + "Other", + "Patent", + "Reference_Item", + "Report", + "Software", + "Sound", + "Standard", + "Thesis_or_Dissertation", + "Unspecified" + ] + }, + "Performance": { + "$ref": "#/components/schemas/IR_Component_Performance" + } + }, + "required": [ + "Data_Type", + "Performance" + ], + "x-stoplight": { + "id": "japuh123zhqkx" + }, + "x-tags": [ + "IR - Item Report" + ] + }, + "IR_Component_Performance": { + "title": "IR - Component Performance", + "type": "object", + "properties": { + "Total_Item_Investigations": { + "$ref": "#/components/schemas/Counts" + }, + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Limit_Exceeded": { + "$ref": "#/components/schemas/Counts" + }, + "No_License": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 1, + "unevaluatedProperties": false, + "x-stoplight": { + "id": "b9ipotg46l7wi" + }, + "x-tags": [ + "IR - Item Report" + ] + }, + "IR_A1_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "IR_A1", + "type": "string" + }, + "Report_Name": { + "const": "Journal Article Requests", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/IR_A1_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "IR_A1 - Report_Header", + "x-stoplight": { + "id": "rlfox1gdh8cti" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "IR_A1", + "Report_Name": "Journal Article Requests", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Article" + ], + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "IR_A1 - Journal Article Requests" + ] + }, + "IR_A1_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Data_Type": { + "type": "array", + "const": [ + "Article" + ], + "items": { + "type": "string" + } + }, + "Metric_Type": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ] + } + } + }, + "required": [ + "Access_Method", + "Data_Type", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "IR_A1 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Article" + ], + "Access_Method": [ + "Regular" + ] + } + ], + "x-stoplight": { + "id": "f3wg2xgn28myp" + }, + "x-tags": [ + "IR_A1 - Journal Article Requests" + ] + }, + "IR_A1_Parent_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Item_Base_Parent_Item" + }, + { + "type": "object", + "properties": { + "Items": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/IR_A1_Report_Item" + } + } + }, + "required": [ + "Items" + ] + } + ], + "unevaluatedProperties": false, + "title": "IR_A1 - Parent_Item", + "x-stoplight": { + "id": "8x47xnajj9hmi" + }, + "x-tags": [ + "IR_A1 - Journal Article Requests" + ], + "examples": [ + { + "Title": "Title 3", + "Item_ID": { + "DOI": "10.9999/xxxxt03", + "Proprietary": "P1:T03", + "Online_ISSN": "1234-4321", + "URI": "https://doi.org/10.9999/xxxxt03" + }, + "Items": [ + { + "Item": "Item 11", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 11" + } + ], + "Publication_Date": "2022-09-19", + "Item_ID": { + "DOI": "10.9999/xxxxi11", + "Proprietary": "P1:I11", + "URI": "https://doi.org/10.9999/xxxxi11" + }, + "Attribute_Performance": [ + { + "Access_Type": "Controlled", + "Performance": { + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + } + ] + }, + { + "Item": "Item 12", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 12" + } + ], + "Publication_Date": "2022-10-01", + "Item_ID": { + "DOI": "10.9999/xxxxi12", + "Proprietary": "P1:I12", + "URI": "https://doi.org/10.9999/xxxxi12" + }, + "Attribute_Performance": [ + { + "Access_Type": "Open", + "Performance": { + "Total_Item_Requests": { + "2022-01": 451, + "2022-02": 743, + "2022-03": 731 + }, + "Unique_Item_Requests": { + "2022-01": 338, + "2022-02": 557, + "2022-03": 548 + } + } + } + ] + } + ] + } + ] + }, + "IR_A1_No_Parent_Item": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/IR_A1_Report_Item" + } + } + }, + "required": [ + "Items" + ], + "title": "IR_A1 - No Parent_Item", + "additionalProperties": false, + "x-stoplight": { + "id": "vrjpryth3yp3g" + }, + "x-tags": [ + "IR_A1 - Journal Article Requests" + ], + "examples": [ + { + "Items": [ + { + "Item": "Item 1", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 1" + } + ], + "Publication_Date": "2022-07-19", + "Item_ID": { + "DOI": "10.9999/xxxxi01", + "Proprietary": "P1:I01", + "URI": "https://doi.org/10.9999/xxxxi01" + }, + "Attribute_Performance": [ + { + "Access_Type": "Open", + "Performance": { + "Total_Item_Requests": { + "2022-01": 877, + "2022-02": 1022, + "2022-03": 885 + }, + "Unique_Item_Requests": { + "2022-01": 658, + "2022-02": 767, + "2022-03": 664 + } + } + } + ] + } + ] + } + ] + }, + "IR_A1_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Item_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/IR_A1_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "IR_A1 - Report_Item", + "x-stoplight": { + "id": "cb9yw8rpbw053" + }, + "x-tags": [ + "IR_A1 - Journal Article Requests" + ], + "examples": [ + { + "Item": "Item 11", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Authors": [ + { + "Name": "Author 11" + } + ], + "Publication_Date": "2022-09-19", + "Item_ID": { + "DOI": "10.9999/xxxxi11", + "Proprietary": "P1:I11", + "URI": "https://doi.org/10.9999/xxxxi11" + }, + "Attribute_Performance": [ + { + "Access_Type": "Controlled", + "Performance": { + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + } + ] + } + ] + }, + "IR_A1_Attribute_Performance": { + "title": "IR_A1 - Attribute_Performance", + "examples": [ + { + "Access_Type": "Controlled", + "Performance": { + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + } + ], + "type": "object", + "additionalProperties": false, + "properties": { + "Access_Type": { + "$ref": "#/components/schemas/Access_Type_Attribute" + }, + "Performance": { + "$ref": "#/components/schemas/IR_A1_Performance" + } + }, + "required": [ + "Access_Type", + "Performance" + ], + "x-stoplight": { + "id": "ao3oinhg6po2r" + }, + "x-tags": [ + "IR_A1 - Journal Article Requests" + ] + }, + "IR_A1_Performance": { + "title": "IR_A1 - Performance", + "examples": [ + { + "Total_Item_Requests": { + "2022-01": 150, + "2022-02": 247, + "2022-03": 243 + }, + "Unique_Item_Requests": { + "2022-01": 113, + "2022-02": 186, + "2022-03": 183 + } + } + ], + "type": "object", + "properties": { + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-stoplight": { + "id": "ad2w7mrqv6lky" + }, + "x-tags": [ + "IR_A1 - Journal Article Requests" + ] + }, + "IR_M1_Report_Header": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Header" + }, + { + "type": "object", + "properties": { + "Report_ID": { + "const": "IR_M1", + "type": "string" + }, + "Report_Name": { + "const": "Multimedia Item Requests", + "type": "string" + }, + "Report_Filters": { + "$ref": "#/components/schemas/IR_M1_Report_Filters" + } + } + } + ], + "unevaluatedProperties": false, + "title": "IR_M1 - Report_Header", + "x-stoplight": { + "id": "ivpo26yy8oj80" + }, + "examples": [ + { + "Release": "5.1", + "Report_ID": "IR_M1", + "Report_Name": "Multimedia Item Requests", + "Created": "2023-02-15T09:11:12Z", + "Created_By": "Sample Publisher", + "Institution_ID": { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + }, + "Institution_Name": "Sample Institution", + "Registry_Record": "https://registry.countermetrics.org/platform/99999999-9999-9999-9999-999999999999", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Audiovisual", + "Image", + "Interactive_Resource", + "Multimedia", + "Sound" + ], + "Access_Method": [ + "Regular" + ] + } + } + ], + "x-tags": [ + "IR_M1 - Multimedia Item Requests" + ] + }, + "IR_M1_Report_Filters": { + "allOf": [ + { + "$ref": "#/components/schemas/Base_Report_Filters" + }, + { + "type": "object", + "properties": { + "Access_Method": { + "type": "array", + "const": [ + "Regular" + ], + "items": { + "type": "string" + } + }, + "Data_Type": { + "type": "array", + "minItems": 5, + "maxItems": 5, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Audiovisual", + "Image", + "Interactive_Resource", + "Multimedia", + "Sound" + ] + } + }, + "Metric_Type": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ] + } + } + }, + "required": [ + "Access_Method", + "Data_Type", + "Metric_Type" + ] + } + ], + "unevaluatedProperties": false, + "title": "IR_M1 - Report_Filters", + "examples": [ + { + "Metric_Type": [ + "Total_Item_Requests", + "Unique_Item_Requests" + ], + "Begin_Date": "2022-01-01", + "End_Date": "2022-03-31", + "Data_Type": [ + "Audiovisual", + "Image", + "Interactive_Resource", + "Multimedia", + "Sound" + ], + "Access_Method": [ + "Regular" + ] + } + ], + "x-stoplight": { + "id": "xlx7is0nyhscl" + }, + "x-tags": [ + "IR_M1 - Multimedia Item Requests" + ] + }, + "IR_M1_No_Parent_Item": { + "type": "object", + "properties": { + "Items": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/IR_M1_Report_Item" + } + } + }, + "required": [ + "Items" + ], + "title": "IR_M1 - No Parent_Item", + "additionalProperties": false, + "x-tags": [ + "IR_M1 - Multimedia Item Requests" + ], + "x-stoplight": { + "id": "n5ux1h3ptq3qy" + }, + "examples": [ + { + "Items": [ + { + "Item": "Item 2", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxi02", + "Proprietary": "P1:I02", + "URI": "https://doi.org/10.9999/xxxxi02" + }, + "Attribute_Performance": [ + { + "Data_Type": "Audiovisual", + "Performance": { + "Total_Item_Requests": { + "2022-01": 767, + "2022-02": 794, + "2022-03": 1096 + }, + "Unique_Item_Requests": { + "2022-01": 575, + "2022-02": 596, + "2022-03": 822 + } + } + } + ] + }, + { + "Item": "Item 9", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxi09", + "Proprietary": "P1:I09", + "URI": "https://doi.org/10.9999/xxxxi09" + }, + "Attribute_Performance": [ + { + "Data_Type": "Image", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1039, + "2022-02": 716, + "2022-03": 1120 + }, + "Unique_Item_Requests": { + "2022-01": 779, + "2022-02": 537, + "2022-03": 840 + } + } + } + ] + }, + { + "Item": "Item 10", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxi10", + "Proprietary": "P1:I10", + "URI": "https://doi.org/10.9999/xxxxi10" + }, + "Attribute_Performance": [ + { + "Data_Type": "Interactive_Resource", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1061, + "2022-02": 1168, + "2022-03": 636 + }, + "Unique_Item_Requests": { + "2022-01": 796, + "2022-02": 876, + "2022-03": 477 + } + } + } + ] + }, + { + "Item": "Item 13", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxi13", + "Proprietary": "P1:I13", + "URI": "https://doi.org/10.9999/xxxxi13" + }, + "Attribute_Performance": [ + { + "Data_Type": "Multimedia", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + } + ] + }, + { + "Item": "Item 22", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxi22", + "Proprietary": "P1:I22", + "URI": "https://doi.org/10.9999/xxxxi22" + }, + "Attribute_Performance": [ + { + "Data_Type": "Sound", + "Performance": { + "Total_Item_Requests": { + "2022-01": 876, + "2022-02": 1143, + "2022-03": 1066 + }, + "Unique_Item_Requests": { + "2022-01": 657, + "2022-02": 857, + "2022-03": 800 + } + } + } + ] + } + ] + } + ] + }, + "IR_M1_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Item_Base_Report_Item" + }, + { + "type": "object", + "properties": { + "Attribute_Performance": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/IR_M1_Attribute_Performance" + } + } + }, + "required": [ + "Attribute_Performance" + ] + } + ], + "unevaluatedProperties": false, + "title": "IR_M1 - Report_Item", + "x-stoplight": { + "id": "qnq117l4ci01k" + }, + "examples": [ + { + "Item": "Item 2", + "Publisher": "Sample Publisher", + "Publisher_ID": { + "ISNI": [ + "4321432143214321" + ] + }, + "Platform": "Platform 1", + "Item_ID": { + "DOI": "10.9999/xxxxi02", + "Proprietary": "P1:I02", + "URI": "https://doi.org/10.9999/xxxxi02" + }, + "Attribute_Performance": [ + { + "Data_Type": "Audiovisual", + "Performance": { + "Total_Item_Requests": { + "2022-01": 767, + "2022-02": 794, + "2022-03": 1096 + }, + "Unique_Item_Requests": { + "2022-01": 575, + "2022-02": 596, + "2022-03": 822 + } + } + } + ] + } + ], + "x-tags": [ + "IR_M1 - Multimedia Item Requests" + ] + }, + "IR_M1_Attribute_Performance": { + "title": "IR_M1 - Attribute_Performance", + "examples": [ + { + "Data_Type": "Multimedia", + "Performance": { + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + } + ], + "type": "object", + "additionalProperties": false, + "properties": { + "Data_Type": { + "type": "string", + "enum": [ + "Audiovisual", + "Image", + "Interactive_Resource", + "Multimedia", + "Sound" + ] + }, + "Performance": { + "$ref": "#/components/schemas/IR_M1_Performance" + } + }, + "required": [ + "Data_Type", + "Performance" + ], + "x-stoplight": { + "id": "8iq0fuv86bpta" + }, + "x-tags": [ + "IR_M1 - Multimedia Item Requests" + ] + }, + "IR_M1_Performance": { + "title": "IR_M1 - Performance", + "examples": [ + { + "Total_Item_Requests": { + "2022-01": 1119, + "2022-02": 1135, + "2022-03": 1024 + }, + "Unique_Item_Requests": { + "2022-01": 839, + "2022-02": 851, + "2022-03": 768 + } + } + ], + "type": "object", + "properties": { + "Total_Item_Requests": { + "$ref": "#/components/schemas/Counts" + }, + "Unique_Item_Requests": { + "$ref": "#/components/schemas/Counts" + } + }, + "minProperties": 2, + "additionalProperties": false, + "x-stoplight": { + "id": "kweyyw1d8oyzx" + }, + "x-tags": [ + "IR_M1 - Multimedia Item Requests" + ] + }, + "Platform_Report_Item": { + "title": "Platform Report_Item", + "x-stoplight": { + "id": "eny72lit396q8" + }, + "type": "object", + "properties": { + "Platform": { + "type": "string", + "minLength": 2 + } + }, + "required": [ + "Platform" + ], + "x-tags": [ + "Shared Models" + ] + }, + "Database_Report_Item": { + "title": "Database Report_Item", + "type": "object", + "x-stoplight": { + "id": "kk6bs9jzwbk7f" + }, + "properties": { + "Database": { + "type": "string", + "minLength": 2 + }, + "Publisher": { + "type": "string" + }, + "Publisher_ID": { + "$ref": "#/components/schemas/Publisher_ID" + }, + "Platform": { + "type": "string", + "minLength": 2 + }, + "Item_ID": { + "$ref": "#/components/schemas/Item_ID" + } + }, + "required": [ + "Database", + "Publisher", + "Platform" + ], + "x-tags": [ + "Shared Models" + ] + }, + "Title_Report_Item": { + "title": "Title Report_Item", + "type": "object", + "properties": { + "Title": { + "type": "string" + }, + "Publisher": { + "type": "string" + }, + "Publisher_ID": { + "$ref": "#/components/schemas/Publisher_ID" + }, + "Platform": { + "type": "string", + "minLength": 2 + }, + "Item_ID": { + "$ref": "#/components/schemas/Item_ID" + } + }, + "required": [ + "Title", + "Publisher", + "Platform" + ], + "x-stoplight": { + "id": "v1s5e6qb88lpg" + }, + "x-tags": [ + "Shared Models" + ] + }, + "Item_Base_Report_Item": { + "title": "Item Base Report_Item", + "type": "object", + "properties": { + "Item": { + "type": "string" + }, + "Publisher": { + "type": "string" + }, + "Publisher_ID": { + "$ref": "#/components/schemas/Publisher_ID" + }, + "Platform": { + "type": "string", + "minLength": 2 + }, + "Item_ID": { + "$ref": "#/components/schemas/Item_ID" + } + }, + "required": [ + "Item", + "Publisher", + "Platform" + ], + "x-stoplight": { + "id": "cm2joryhidldu" + }, + "x-tags": [ + "Shared Models" + ] + }, + "Item_Report_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Item_Base_Report_Item" + }, + { + "type": "object", + "properties": { + "Authors": { + "$ref": "#/components/schemas/Authors" + }, + "Publication_Date": { + "type": "string", + "format": "date" + }, + "Article_Version": { + "$ref": "#/components/schemas/Article_Version" + } + } + } + ], + "title": "Item Report_Item", + "x-tags": [ + "Shared Models" + ], + "x-stoplight": { + "id": "yp0wlwrejkf2y" + } + }, + "Item_Base_Parent_Item": { + "title": "Item Base Parent_Item", + "type": "object", + "x-stoplight": { + "id": "hauuga1wt2mru" + }, + "properties": { + "Title": { + "type": "string" + }, + "Authors": { + "$ref": "#/components/schemas/Authors" + }, + "Article_Version": { + "$ref": "#/components/schemas/Article_Version" + }, + "Item_ID": { + "$ref": "#/components/schemas/Item_ID" + } + }, + "required": [ + "Item_ID" + ], + "x-tags": [ + "Shared Models" + ] + }, + "Item_Parent_Item": { + "allOf": [ + { + "$ref": "#/components/schemas/Item_Base_Parent_Item" + }, + { + "type": "object", + "properties": { + "Publication_Date": { + "type": "string", + "format": "date" + }, + "Data_Type": { + "type": "string", + "enum": [ + "Book", + "Conference", + "Database_Full", + "Journal", + "Newspaper_or_Newsletter", + "Reference_Work" + ] + } + } + } + ], + "title": "Item Parent_Item", + "x-tags": [ + "Shared Models" + ], + "x-stoplight": { + "id": "1iynm7tzg0428" + } + }, + "Item_Component_Item": { + "title": "Item Component_Item", + "type": "object", + "x-stoplight": { + "id": "wz1kaijloz4sq" + }, + "properties": { + "Item": { + "type": "string" + }, + "Item_ID": { + "$ref": "#/components/schemas/Item_ID" + }, + "Authors": { + "$ref": "#/components/schemas/Authors" + }, + "Publication_Date": { + "type": "string", + "format": "date" + }, + "Article_Version": { + "$ref": "#/components/schemas/Article_Version" + } + }, + "required": [ + "Item_ID" + ], + "x-tags": [ + "Shared Models" + ] + }, + "Access_Method_Attribute": { + "title": "Access_Method Attribute", + "x-stoplight": { + "id": "50e6jks4bxclh" + }, + "type": "string", + "enum": [ + "Regular", + "TDM" + ], + "x-tags": [ + "Shared Models" + ] + }, + "Access_Method_Filter": { + "title": "Access_Method Filter", + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "Regular", + "TDM" + ] + }, + "x-stoplight": { + "id": "2zzgyzwsjwlbe" + }, + "x-tags": [ + "Shared Models" + ] + }, + "Access_Type_Attribute": { + "title": "Access_Type Attribute", + "type": "string", + "enum": [ + "Controlled", + "Open", + "Free_To_Read" + ], + "x-stoplight": { + "id": "jhzf1yo3td32n" + }, + "x-tags": [ + "Shared Models" + ] + }, + "Access_Type_Filter": { + "title": "Access_Type Filter", + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "type": "string", + "enum": [ + "Controlled", + "Free_To_Read", + "Open" + ] + }, + "x-stoplight": { + "id": "e7s113x9vgjd8" + }, + "x-tags": [ + "Shared Models" + ] + }, + "Granularity_Attribute": { + "title": "Granularity Attribute", + "x-stoplight": { + "id": "ybell2vsau1un" + }, + "type": "object", + "properties": { + "Granularity": { + "type": "string", + "enum": [ + "Month", + "Totals" + ] + } + }, + "x-tags": [ + "Shared Models" + ] + }, + "Common_Extensions_Filters": { + "title": "Common Extensions Filters", + "type": "object", + "x-stoplight": { + "id": "qwryex01htown" + }, + "properties": { + "Attributed": { + "type": "string", + "enum": [ + "No", + "Yes" + ] + }, + "Country_Code": { + "type": "string", + "pattern": "^[A-Z]{2}$" + }, + "Subdivision_Code": { + "type": "string", + "pattern": "^[A-Z]{2}-[A-Z0-9]{1,3}$" + } + }, + "x-tags": [ + "Shared Models" + ] + }, + "Common_Extensions_Attributes": { + "title": "Common Extensions Attributes", + "type": "object", + "x-tags": [ + "Shared Models" + ], + "x-stoplight": { + "id": "0uwj2pnp66xvr" + }, + "properties": { + "Customer_ID": { + "type": "string", + "minLength": 2, + "description": "When a COUNTER Report contains usage for multiple organizations, this element can be used to break the usage down by institution. This is a common extension, support is optional (see Section 11 of the Code of Practice for details)." + }, + "Institution_Name": { + "type": "string", + "minLength": 2, + "description": "When a COUNTER Report contains usage for multiple organizations, this element can be used to break the usage down by institution. This is a common extension, support is optional (see Section 11 of the Code of Practice for details)." + }, + "Country_Name": { + "type": "string", + "minLength": 2, + "description": "Name of the country according to ISO 3166-1. This is a common extension, support is optional (see Section 11 of the Code of Practice for details)." + }, + "Country_Code": { + "type": "string", + "pattern": "^[A-Z]{2}$", + "description": "ISO 3166-1 alpha-2 code of the country. This is a common extension, support is optional (see Section 11 of the Code of Practice for details)." + }, + "Subdivision_Name": { + "type": "string", + "minLength": 2, + "description": "Name of the country subdivision according to ISO 3166-2. This is a common extension, support is optional (see Section 11 of the Code of Practice for details)." + }, + "Subdivision_Code": { + "type": "string", + "pattern": "^[A-Z]{2}-[A-Z0-9]{1,3}$", + "description": "ISO 3166-2 code of the country subdivision. This is a common extension, support is optional (see Section 11 of the Code of Practice for details)." + }, + "Attributed": { + "type": "string", + "description": "Whether the report provider was able to attribute the usage to an institution or not. This is a common extension, support is optional (see Section 11 of the Code of Practice for details).", + "enum": [ + "Yes", + "No" + ] + } + } + }, + "Authors": { + "title": "Authors", + "type": "array", + "uniqueItems": true, + "minItems": 1, + "maxItems": 3, + "x-stoplight": { + "id": "spisn0q3toc41" + }, + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "minLength": 2 + }, + "ISNI": { + "type": "string", + "pattern": "^[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{3}[0-9X]$" + }, + "ORCID": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]$" + } + }, + "required": [ + "Name" + ] + }, + "x-tags": [ + "Shared Models" + ] + }, + "Article_Version": { + "title": "Article_Version", + "type": "string", + "x-stoplight": { + "id": "v9lspau3grn3r" + }, + "enum": [ + "AO", + "SMUR", + "AM", + "P", + "VoR", + "CVoR", + "EVoR" + ], + "x-tags": [ + "Shared Models" + ] + }, + "Organization_ID": { + "title": "Organization_ID", + "type": "object", + "properties": { + "ISNI": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{4}[ -]?[0-9]{3}[0-9X]$" + } + }, + "ROR": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "type": "string", + "pattern": "^0[a-z0-9]{6}[0-9]{2}$" + } + }, + "Proprietary": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[a-zA-Z][a-zA-Z0-9_./]{0,15}[a-zA-Z0-9]:.+" + } + } + }, + "x-stoplight": { + "id": "hsbita5bb4hwo" + }, + "x-tags": [ + "Shared Models" + ] + }, + "Institution_ID": { + "allOf": [ + { + "$ref": "#/components/schemas/Organization_ID" + }, + { + "type": "object", + "properties": { + "ISIL": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "type": "string", + "pattern": "^([A-Z]{2}|[a-zA-Z0-9]|[a-zA-Z0-9]{3,4})-.{1,11}$" + } + }, + "OCLC": { + "type": "array", + "uniqueItems": true, + "minItems": 1, + "items": { + "type": "string", + "pattern": "^[0-9]+$" + } + } + } + } + ], + "minProperties": 1, + "unevaluatedProperties": false, + "title": "Institution_ID", + "examples": [ + { + "Proprietary": [ + "P1:1234" + ], + "ISNI": [ + "1234123412341234" + ] + } + ], + "x-tags": [ + "Shared Models" + ], + "x-stoplight": { + "id": "g7pm8phxq9kga" + } + }, + "Publisher_ID": { + "allOf": [ + { + "$ref": "#/components/schemas/Organization_ID" + } + ], + "minProperties": 1, + "unevaluatedProperties": false, + "title": "Publisher_ID", + "examples": [ + { + "ISNI": [ + "4321432143214321" + ] + } + ], + "x-tags": [ + "Shared Models" + ], + "x-stoplight": { + "id": "xo56r5dpr9rx4" + } + }, + "Item_ID": { + "x-stoplight": { + "id": "hr3sz3guls9vx" + }, + "type": "object", + "properties": { + "DOI": { + "type": "string", + "pattern": "^10\\.[1-9][0-9]{2}[0-9.]*\\/.+$" + }, + "ISBN": { + "type": "string", + "pattern": "^97[89]-[0-9]+-[0-9]+-[0-9]+-[0-9]$", + "minLength": 17, + "maxLength": 17 + }, + "Online_ISSN": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{3}[0-9X]$" + }, + "Print_ISSN": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{3}[0-9X]$" + }, + "Proprietary": { + "type": "string", + "pattern": "^[a-zA-Z][a-zA-Z0-9_./]{0,15}[a-zA-Z0-9]:.+" + }, + "URI": { + "type": "string", + "format": "uri" + } + }, + "minProperties": 1, + "additionalProperties": false, + "title": "Item_ID", + "examples": [ + { + "DOI": "10.9999/xxxxt01", + "Proprietary": "P1:T01", + "ISBN": "979-8-88888-888-8", + "URI": "https://doi.org/10.9999/xxxxt01" + } + ], + "x-tags": [ + "Shared Models" + ] + }, + "Counts": { + "title": "Counts", + "x-stoplight": { + "id": "2royhrgy5355l" + }, + "type": "object", + "patternProperties": { + "^[0-9]{4}-(0[1-9]|1[0-2])$": { + "type": "integer", + "minimum": 0 + } + }, + "minProperties": 1, + "additionalProperties": false, + "examples": [ + { + "2022-01": 1461, + "2022-02": 1704, + "2022-03": 1475 + } + ], + "x-tags": [ + "Shared Models" + ] + }, + "Exception": { + "title": "Exception", + "x-stoplight": { + "id": "7ccbfsfe7nrev" + }, + "type": "object", + "examples": [ + { + "Code": 3030, + "Message": "No Usage Available for Requested Dates" + } + ], + "properties": { + "Code": { + "type": "integer", + "description": "Exception Code. See Table D.1 in the Code of Practice, Appendix D." + }, + "Message": { + "type": "string", + "minLength": 2, + "description": "Exception Message. See Table D.1 in the Code of Practice, Appendix D." + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "x-tags": [ + "Exceptions" + ] + }, + "Exception_0": { + "title": "Exception 0", + "type": "object", + "examples": [], + "properties": { + "Code": { + "type": "integer", + "const": 0 + }, + "Message": { + "type": "string", + "minLength": 2, + "description": "Exception Message." + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "qhslmg8xuwaqn" + }, + "description": "Info or Debug Message. These Messages will never be standardized and service providers can design them as they see fit.\n\nNote: The standard Exceptions with Code > 999 MUST be used for the indicated invocation conditions, it is neither permitted to use custom Exceptions with Code <= 999 instead nor to define custom Exceptions with Code > 999." + }, + "Exception_1-999": { + "title": "Exception 1-999", + "type": "object", + "examples": [], + "additionalProperties": false, + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "jthaeph98p1yr" + }, + "description": "Warning Message. This range is reserved for the use of service providers to supply their own custom warnings.\n\nNote: The standard Exceptions with Code > 999 MUST be used for the indicated invocation conditions, it is neither permitted to use custom Exceptions with Code <= 999 instead nor to define custom Exceptions with Code > 999.", + "properties": { + "Code": { + "type": "integer", + "description": "Exception Code.", + "minimum": 1, + "maximum": 999 + }, + "Message": { + "type": "string", + "minLength": 2, + "description": "Exception Message." + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ] + }, + "Exception_1000": { + "title": "Exception 1000", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 1000 + }, + "Message": { + "type": "string", + "const": "Service Not Available" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "x-stoplight": { + "id": "nnsvckcwhhrpn" + }, + "description": "The service is executing a request, but due to internal errors cannot complete the request. If possible, the server should provide an explanation in the additional Data element.", + "x-tags": [ + "Exceptions" + ] + }, + "Exception_1010": { + "title": "Exception 1010", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 1010 + }, + "Message": { + "type": "string", + "const": "Service Busy" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "The service is too busy to execute the incoming request. The client should retry the request after some reasonable time.", + "x-stoplight": { + "id": "ffu7dpwk712cc" + }, + "x-tags": [ + "Exceptions" + ] + }, + "Exception_1011": { + "title": "Exception 1011", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 1011 + }, + "Message": { + "type": "string", + "const": "Report Queued for Processing" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "Services queueing incoming report requests must return a response with this Exception and no payload to inform the client about the processing status. The client should retry the request after some reasonable time.", + "x-stoplight": { + "id": "s4dlpxqx1h4ij" + }, + "x-tags": [ + "Exceptions" + ] + }, + "Exception_1020": { + "title": "Exception 1020", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 1020 + }, + "Message": { + "type": "string", + "const": "Client has made too many requests" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "If the service sets a limit on the number of requests a client can make within a given timeframe, the server will return this Exception when the client exceeds that limit. The server would provide an explanation of the limit in the additional Data element.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "f03gj10soo9rl" + } + }, + "Exception_1030": { + "title": "Exception 1030", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 1030 + }, + "Message": { + "type": "string", + "const": "Insufficient Information to Process Request" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "There is insufficient data in the request to begin processing (e.g. missing requestor_id, no customer_id, etc.).", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "tosiypt95o4x3" + } + }, + "Exception_2000": { + "title": "Exception 2000", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 2000 + }, + "Message": { + "type": "string", + "const": "Requestor Not Authorized to Access Service" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "If requestor_id is not recognized or not authorized by the service.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "gz2d7p3c84tr2" + } + }, + "Exception_2010": { + "title": "Exception 2010", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 2010 + }, + "Message": { + "type": "string", + "const": "Requestor is Not Authorized to Access Usage for Institution" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "If requestor_id has not been authorized to harvest usage for the institution identified by the customer_id, or if the customer_id is not recognized.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "pbruj42fy8lwk" + } + }, + "Exception_2011": { + "title": "Exception 2011", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 2011 + }, + "Message": { + "type": "string", + "const": "Global Reports Not Supported" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "Reporting to “The World”, customer_id 0000000000000000, is not supported.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "mc0mh59ti0e08" + } + }, + "Exception_2020": { + "title": "Exception 2020", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 2020 + }, + "Message": { + "type": "string", + "const": "APIKey Invalid" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "The service requires a valid APIKey to access usage data and the key provided was not valid or not authorized for the data being requested.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "pbsdlraqtnxjc" + } + }, + "Exception_3020": { + "title": "Exception 3020", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 3020 + }, + "Message": { + "type": "string", + "const": "Invalid Date Arguments" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "Any format or logic errors involving date computations (e.g., end_date cannot be less than begin_date).", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "mqzhrgrtz7aqw" + } + }, + "Exception_3030": { + "title": "Exception 3030", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 3030 + }, + "Message": { + "type": "string", + "const": "No Usage Available for Requested Dates" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "The service did not find any data for the specified date range and other filters (if any).\n\nNote: If the usage for a requested month either hasn’t been processed yet or is no longer available, only Exception 3031 or 3032 must be returned for that month.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "bb45dgmzs67hj" + } + }, + "Exception_3031": { + "title": "Exception 3031", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 3031 + }, + "Message": { + "type": "string", + "const": "Usage Not Ready for Requested Dates" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "The service has not yet processed the usage for one or more of the requested months, if some months are available that data should be returned. The Exception should include the months not processed in the additional Data element.\n\nNote: If the requested begin_date is the current or a future month, the server should return Exception 3020. If the requested end_date is the current or a future month, the server may continue processing the request and include Exception 3031, the End_Date Report_Filter then should be set to the previous month (the last month that could have been processed).", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "n91svprzbw78b" + } + }, + "Exception_3032": { + "title": "Exception 3032", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 3032 + }, + "Message": { + "type": "string", + "const": "Usage No Longer Available for Requested Dates" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "The service does not have the usage for one or more of the requested months because the requested begin_date is earlier than the first month for which data has been processed and is available. If some months are available that data should be returned. The Exception should include the information about the months processed and available in the additional Data element.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "2tu8ef3qknv8i" + } + }, + "Exception_3040": { + "title": "Exception 3040", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 3040 + }, + "Message": { + "type": "string", + "const": "Partial Data Returned" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "The request could not be fulfilled in its entirety, since some of the requested data is missing. The server should return the available data and provide an explanation in the additional Data element.\n\nNote: This Exception is not intended for the conditions already covered by Exceptions 3030, 3031 and 3032. A use case for this Exception for example would be that usage data is missing because the logging has failed. Usually this Exception indicates a permanent error.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "lq1qrf91ykx82" + } + }, + "Exception_3050": { + "title": "Exception 3050", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 3050 + }, + "Message": { + "type": "string", + "const": "Parameter Not Recognized in this Context" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "The request contained one or more parameters that are not recognized by the server in the context of the report being serviced. The server should list the names of unsupported parameters in the additional Data element.\n\nNote: The server is expected to ignore unsupported parameters and continue to process the request, returning data that is available without the parameter being applied.\n\nNote: This Exception is only applicable for report requests. For report list, member list and server status requests parameters not recognized by the server should be ignored.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "2nk2t4j11vwmj" + } + }, + "Exception_3060": { + "title": "Exception 3060", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 3060 + }, + "Message": { + "type": "string", + "const": "Invalid ReportFilter Value" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "The request contained one or more filter values that are not supported by the server. The server should list the names of unsupported filter values in the additional Data element.\n\nNote: The server is expected to ignore unsupported filters and continue to process the request, returning data that is available without the filter being applied.\n\nNote: If the begin_date or end_date value is invalid, the server must return Exception 3020. If the service requires a platform parameter, and the platform value is invalid, the server should return Exception 1030.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "l8uzfngogq6ui" + } + }, + "Exception_3061": { + "title": "Exception 3061", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 3061 + }, + "Message": { + "type": "string", + "const": "Incongruous ReportFilter Value" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "A filter element includes multiple values in a pipe-delimited list; however, the supplied values are not all of the same scope (e.g., item_id filter includes article level DOIs and journal level DOIs or ISSNs).\n\nNote: The server is expected to ignore the invalid filters and continue to process the request, returning data that is available without the filter being applied.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "0zv5nln3b8785" + } + }, + "Exception_3062": { + "title": "Exception 3062", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 3062 + }, + "Message": { + "type": "string", + "const": "Invalid ReportAttribute Value" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "The request contained one or more report attribute values that are not supported by the server. The server should list the names of unsupported report attribute values in the additional Data element.\n\nNote: The server is expected to ignore unsupported report attributes and continue to process the request, returning data that is available without the report attribute being applied.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "bo5ndiiwc33ep" + } + }, + "Exception_3063": { + "title": "Exception 3063", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 3063 + }, + "Message": { + "type": "string", + "const": "Components Not Supported" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "The request contained include_component_details=True, but reporting on component usage is not supported.\n\nNote: The server is expected to ignore unsupported report attributes and continue to process the request, returning data that is available without the report attribute being applied.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "ka483mok43fd8" + } + }, + "Exception_3070": { + "title": "Exception 3070", + "type": "object", + "properties": { + "Code": { + "type": "integer", + "const": 3070 + }, + "Message": { + "type": "string", + "const": "Required ReportFilter Missing" + }, + "Help_URL": { + "type": "string", + "description": "URL to a help page that explains the Exception in more detail.", + "format": "uri" + }, + "Data": { + "type": "string", + "description": "Additional data provided by the server to clarify the Exception." + } + }, + "required": [ + "Code", + "Message" + ], + "additionalProperties": false, + "description": "A required filter was not included in the request. Which filters are required will depend on the report and the service being called. The server should list the names of the missing filters in the additional Data element.\n\nNote: If begin_date or end_date is missing, the server must return Exception 1030. If the service requires a platform parameter, and platform is missing, the server also should return Exception 1030.\n\nNote: Currently there are no other required report filters, so this Exception should not occur.", + "x-tags": [ + "Exceptions" + ], + "x-stoplight": { + "id": "69tm0fqacc7xf" + } + } + }, + "examples": {}, + "responses": { + "503_Exception": { + "description": "Exception response for HTTP status code 503", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Exception_1000" + }, + { + "$ref": "#/components/schemas/Exception_1010" + } + ] + } + } + } + }, + "202_Exception": { + "description": "Exception response for HTTP status code 202", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Exception_1011" + } + } + } + }, + "200_PR_P1": { + "description": "Standard View PR_P1 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PR_P1" + } + } + } + }, + "200_PR": { + "description": "COUNTER Report PR response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PR" + } + } + } + }, + "200_DR": { + "description": "COUNTER Report DR response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DR" + } + } + } + }, + "200_DR_D1": { + "description": "Standard View DR_D1 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DR_D1" + } + } + } + }, + "200_DR_D2": { + "description": "Standard View DR_D2 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DR_D2" + } + } + } + }, + "200_TR": { + "description": "COUNTER Report TR response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TR" + } + } + } + }, + "200_TR_B1": { + "description": "Standard View TR_B1 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TR_B1" + } + } + } + }, + "200_TR_B2": { + "description": "Standard View TR_B2 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TR_B2" + } + } + } + }, + "200_TR_B3": { + "description": "Standard View TR_B3 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TR_B3" + } + } + } + }, + "200_TR_J1": { + "description": "Standard View TR_J1 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TR_J1" + } + } + } + }, + "200_TR_J2": { + "description": "Standard View TR_J2 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TR_J2" + } + } + } + }, + "200_TR_J3": { + "description": "Standard View TR_J3 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TR_J3" + } + } + } + }, + "200_TR_J4": { + "description": "Standard View TR_J4 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TR_J4" + } + } + } + }, + "200_IR_M1": { + "description": "Standard View IR_M1 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IR_M1" + } + } + } + }, + "200_IR_A1": { + "description": "Standard View IR_A1 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IR_A1" + } + } + } + }, + "200_IR": { + "description": "COUNTER Report IR response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IR" + } + } + } + }, + "200_Status": { + "description": "Server Status response", + "content": { + "application/json": { + "schema": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/components/schemas/Status" + } + } + } + } + }, + "200_Members": { + "description": "Member List response", + "content": { + "application/json": { + "schema": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/components/schemas/Member" + } + } + } + } + }, + "200_Platforms": { + "description": "Platform List response", + "content": { + "application/json": { + "schema": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/components/schemas/Platform" + } + }, + "examples": { + "Example 1": { + "value": [ + { + "Platform_Parameter": "12", + "Platform_Name": "EBSCOhost" + }, + { + "Platform_Parameter": "23", + "Platform_Name": "ProQuest", + "Platform_Description": "Reports for the www.proquest.com platform" + }, + { + "Platform_Parameter": "41", + "Platform_Name": "ScienceDirect" + } + ] + } + } + } + } + }, + "200_Reports": { + "description": "Report List response", + "content": { + "application/json": { + "schema": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/components/schemas/Report" + } + }, + "examples": { + "Example 1": { + "value": [ + { + "Report_Name": "Platform Report", + "Report_ID": "PR", + "Release": "5.1", + "Report_Description": "A customizable report summarizing activity across a report provider’s platforms that allows the user to apply filters and select other configuration options.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Platform Usage", + "Report_ID": "PR_P1", + "Release": "5.1", + "Report_Description": "Platform-level usage summarized by Metric_Type.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Database Report", + "Report_ID": "DR", + "Release": "5.1", + "Report_Description": "A customizable report detailing activity by database that allows the user to apply filters and select other configuration options.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Database Search and Item Usage", + "Report_ID": "DR_D1", + "Release": "5.1", + "Report_Description": "Reports on key Searches, Investigations and Requests metrics needed to evaluate a database.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Database Access Denied", + "Report_ID": "DR_D2", + "Release": "5.1", + "Report_Description": "Reports on Access Denied activity for databases where users were denied access because simultaneous-use licenses were exceeded or their institution did not have a license for the database.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Title Report", + "Report_ID": "TR", + "Release": "5.1", + "Report_Description": "A customizable report detailing activity at the title level (journal, book, etc.) that allows the user to apply filters and select other configuration options.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Book Requests (Controlled)", + "Report_ID": "TR_B1", + "Release": "5.1", + "Report_Description": "Reports on full-text activity for books, excluding Open and Free_To_Read content, as Total_Item_Requests and Unique_Title_Requests.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Book Access Denied", + "Report_ID": "TR_B2", + "Release": "5.1", + "Report_Description": "Reports on Access Denied activity for books where users were denied access because simultaneous-use licenses were exceeded or their institution did not have a license for the book.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Book Usage by Access Type", + "Report_ID": "TR_B3", + "Release": "5.1", + "Report_Description": "Reports on book usage showing all applicable Metric_Types broken down by Access_Type.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Journal Requests (Controlled)", + "Report_ID": "TR_J1", + "Release": "5.1", + "Report_Description": "Reports on usage of journal content, excluding Open and Free_To_Read content, as Total_Item_Requests and Unique_Item_Requests.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Journal Access Denied", + "Report_ID": "TR_J2", + "Release": "5.1", + "Report_Description": "Reports on Access Denied activity for journal content where users were denied access because simultaneous-use licenses were exceeded or their institution did not have a license for the title.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Journal Usage by Access Type", + "Report_ID": "TR_J3", + "Release": "5.1", + "Report_Description": "Reports on usage of journal content for all Metric_Types broken down by Access_Type.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Journal Requests by YOP (Controlled)", + "Report_ID": "TR_J4", + "Release": "5.1", + "Report_Description": "Breaks down the usage of journal content, excluding Open and Free_To_Read content, by year of publication (YOP), providing counts for the Metric_Types Total_Item_Requests and Unique_Item_Requests.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Item Report", + "Report_ID": "IR", + "Release": "5.1", + "Report_Description": "A granular, customizable report showing activity at the level of the item (article, chapter, media object, etc.) that allows the user to apply filters and select other configuration options.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Journal Article Requests", + "Report_ID": "IR_A1", + "Release": "5.1", + "Report_Description": "Reports on journal article requests at the article level. This report is limited to content with a Data_Type of Article and Metric_Types of Total_Item_Requests and Unique_Item_Requests.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + }, + { + "Report_Name": "Multimedia Item Requests", + "Report_ID": "IR_M1", + "Release": "5.1", + "Report_Description": "Reports on multimedia requests at the item level.", + "First_Month_Available": "2024-01", + "Last_Month_Available": "2024-11" + } + ] + } + } + } + } + }, + "429_Exception": { + "description": "Exception response for HTTP status code 429", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Exception_1020" + } + } + } + }, + "400_Exception": { + "description": "Exception response for HTTP status code 400", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Exception_1030" + }, + { + "$ref": "#/components/schemas/Exception_3020" + } + ] + } + } + } + }, + "401_Exception": { + "description": "Exception response for HTTP status code 401", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Exception_2000" + }, + { + "$ref": "#/components/schemas/Exception_2020" + } + ] + } + } + } + }, + "403_Exception": { + "description": "Exception response for HTTP status code 403", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Exception_2010" + }, + { + "$ref": "#/components/schemas/Exception_2011" + } + ] + } + } + } + }, + "400_Exception_1030": { + "description": "Exception response for HTTP status code 400", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Exception_1030" + } + } + } + } + }, + "parameters": { + "customer_id": { + "name": "customer_id", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "description": "Identifies the customer to provide report data for." + }, + "platform": { + "name": "platform", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Name of the platform the report data is being requested for. Might be required if a COUNTER API server provides usage data for multiple platforms." + }, + "requestor_id": { + "name": "requestor_id", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Identifier which may be assigned by a report provider to the requester (e.g. institution or consortium) that is asking for usage data." + }, + "api_key": { + "name": "api_key", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Unique code which may be assigned by a report provider to grant access to this API." + }, + "begin_date": { + "name": "begin_date", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "description": "Reporting period start date in the form of yyyy-mm-dd or yyyy-mm." + }, + "end_date": { + "name": "end_date", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "description": "Reporting period end date in the form of yyyy-mm-dd or yyyy-mm." + }, + "metric_type": { + "name": "metric_type", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Optional Report_Filter. A list of Metric_Types separated by the | character (URL encoded as %7C) to return usage for. Omit this parameter to include all Metric_Types." + }, + "data_type": { + "name": "data_type", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Optional Report_Filter. A list of Data_Types separated by the | character (URL encoded as %7C) to return usage for. Omit this parameter to include all Data_Types." + }, + "access_method": { + "name": "access_method", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "Regular", + "TDM" + ] + }, + "description": "Optional Report_Filter. A list of Access_Methods separated by the | character (URL encoded as %7C) to return usage for. Omit this parameter to include all Access_Methods." + }, + "attributes_to_show": { + "name": "attributes_to_show", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Optional Report_Attribute. A list of attributes separated by the | character (URL encoded as %7C) to include as separate elements in the report. When an element is included, the usage will be presented for each value of that element, otherwise the usage will be rolled up for all values." + }, + "granularity": { + "name": "granularity", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "Month", + "Totals" + ] + }, + "description": "Optional Report_Attribute. Include this parameter to retrieve usage with a granularity of Month (default if omitted) or Totals (usage not broken down by months). Support for Totals is recommended but OPTIONAL, i.e. report providers can decide whether to support the value or not." + }, + "database": { + "name": "database", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Name of a specific database usage is being requested for. If omitted, all databases on the platform with usage for the customer will be returned. Support for this filter is recommended but OPTIONAL, i.e. report providers can decide whether to support the filter or not." + }, + "access_type": { + "name": "access_type", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "Controlled", + "Open", + "Free_To_Read" + ] + }, + "description": "Optional Report_Filter. A list of Access_Types separated by the | character (URL encoded as %7C) to return usage for. Omit this parameter to include all Access_Types." + }, + "yop": { + "name": "yop", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Optional Report_Filter. A list of ranges of years of publication in the format of yyyy or yyyy-yyyy separated by the | character (URL encoded as %7C) to return usage for. Omit this parameter to include all years of publication." + }, + "item_id": { + "name": "item_id", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Identifier of a specific title usage is being requested for. If omitted, all titles on the platform with usage for the customer will be returned. Support for this filter is recommended but OPTIONAL, i.e. report providers can decide whether to support the filter or not." + }, + "author": { + "name": "author", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Identifier of a specific author usage is being requested for. If omitted, all items on the platform with usage for the customer will be returned. Support for this filter is recommended but OPTIONAL, i.e. report providers can decide whether to support the filter or not." + }, + "include_component_details": { + "name": "include_component_details", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "False", + "True" + ] + }, + "description": "Optional Report_Attribute. Include this parameter with a value of True to include details of the item components in the report (if supported by the COUNTER API server). Omitting the parameter will exclude the item component elements." + }, + "include_parent_details": { + "name": "include_parent_details", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "False", + "True" + ] + }, + "description": "Optional Report_Attribute. Include this parameter with a value of True to include details of the item parents in the report, e.g. information about the journal in which an article is published. Omitting the parameter will exclude the item parent elements." + }, + "attributed": { + "name": "attributed", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "Yes", + "No" + ] + }, + "description": "Optional Report_Filter. Include this parameter to retrieve usage (not) attributed to institutions. Attributed is a common extension, support is optional (see Section 11 of the Code of Practice for details)." + }, + "country_code": { + "name": "country_code", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Optional Report_Filter. A list of Country_Codes separated by the | character (URL encoded as %7C) to return usage for. Country_Code is a common extension, support is optional (see Section 11 of the Code of Practice for details)." + }, + "subdivision_code": { + "name": "subdivision_code", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Optional Report_Filter. A list of Subdivision_Codes separated by the | character (URL encoded as %7C) to return usage for. Subdivision_Code is a common extension, support is optional (see Section 11 of the Code of Practice for details)." + }, + "include_month_details": { + "name": "include_month_details", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "False", + "True" + ] + }, + "description": "Use a value of True to request the Month_Details common extension (see Section 11.7 of the Code of Practice for details). Support for common extensions is OPTIONAL, i.e. report providers can decide whether to support the extension or not." + } + }, + "securitySchemes": {} + }, + "x-internal": false +} diff --git a/counter_api/tests/schema.py b/counter_api/tests/schema.py new file mode 100644 index 00000000..4714cfaa --- /dev/null +++ b/counter_api/tests/schema.py @@ -0,0 +1,26 @@ +import json +from functools import lru_cache +from pathlib import Path + +from jsonschema.validators import validator_for + +SCHEMA_PATH = Path(__file__).parents[1] / "schemas" / "COUNTER_API_5_1_1.json" + + +@lru_cache(maxsize=1) +def _specification(): + with SCHEMA_PATH.open(encoding="utf-8") as stream: + return json.load(stream) + + +def validate_report(payload, report_id): + specification = _specification() + schema_name = report_id.upper() + schema = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "components": specification["components"], + "$ref": f"#/components/schemas/{schema_name}", + } + validator = validator_for(schema)(schema) + + validator.validate(payload) From 1d6e32d0c82c09693d60607b72450d52e2bd819f Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:39 -0300 Subject: [PATCH 21/47] =?UTF-8?q?Transforma=20agrega=C3=A7=C3=B5es=20em=20?= =?UTF-8?q?entradas=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seleciona métricas por escopo, associa metadados e consolida as contagens mensais sem duplicar filtros do OpenSearch. --- counter_api/report_entries.py | 223 ++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 counter_api/report_entries.py diff --git a/counter_api/report_entries.py b/counter_api/report_entries.py new file mode 100644 index 00000000..e58b0ae6 --- /dev/null +++ b/counter_api/report_entries.py @@ -0,0 +1,223 @@ +from collections import defaultdict +from time import monotonic + +from django.conf import settings + +from counter_api.constants import METRICS, STANDARD_VIEWS, TITLE_METRICS +from counter_api.exceptions import service_unavailable +from counter_api.identifiers import counter_identifiers + +ATTRIBUTE_FIELDS = { + "Data_Type": "data_type", + "YOP": "yop", + "Access_Type": "access_type", + "Access_Method": "access_method", +} +PARENT_DATA_TYPES = {"journals": "Journal", "books": "Book"} + + +def build_entries(report_id, platform, records, params, deadline): + entries = defaultdict(lambda: defaultdict(int)) + shown_attributes = _shown_attributes(report_id, params) + missing_sources = set() + missing_documents = set() + + for bucket, source, document in records: + if deadline and monotonic() >= deadline: + raise service_unavailable() + + key = bucket["key"] + if report_id.startswith("tr") and not source: + missing_sources.add(key.get("source_key")) + continue + if report_id == "ir" and not document: + missing_documents.add(key.get("document_key")) + continue + + for metric_type, field in _metrics_for_bucket( + report_id, + platform, + key, + params, + ): + count = int(bucket.get(field, {}).get("value", 0)) + if count <= 0: + continue + + entry = _entry(report_id, platform, key, source, document, metric_type) + if ( + params.get("Data_Type") + and entry["data_type"] not in params["Data_Type"] + ): + continue + + entry = _hide_attributes(entry, shown_attributes) + identity = _identity(report_id, entry) + month = _month_value(key.get("month")) + entries[identity][month] += count + + if len(entries) > settings.COUNTER_MAX_REPORT_ENTRIES: + raise service_unavailable("report size") + + result = [] + for identity, performance in entries.items(): + entry = dict(identity) + entry["performance"] = dict(performance) + result.append(entry) + + exceptions = [] + if missing_sources: + exceptions.append( + { + "Code": 3040, + "Message": "Partial Data Returned", + "Data": f"Metadata missing for {len(missing_sources)} title(s)", + } + ) + if missing_documents: + exceptions.append( + { + "Code": 3040, + "Message": "Partial Data Returned", + "Data": f"Metadata missing for {len(missing_documents)} item(s)", + } + ) + + return result, exceptions + + +def _metrics_for_bucket(report_id, platform, key, params): + scope = key.get("metric_scope") or "item" + if platform.collection_type == "books": + if report_id == "ir": + metrics = METRICS if scope == "item" else {} + elif scope == "title": + metrics = TITLE_METRICS + else: + metrics = METRICS + elif scope == "item": + metrics = METRICS + else: + metrics = {} + + selected = params.get("Metric_Type") + return ( + (metric_type, field) + for metric_type, field in metrics.items() + if not selected or metric_type in selected + ) + + +def _entry(report_id, platform, key, source, document, metric_type): + data_type = _data_type(report_id, platform, key) + source_ids = () + item_ids = () + yop = "" + + if report_id.startswith("tr"): + source_ids = counter_identifiers(source) + yop = f"{key.get('publication_year') or 1:04d}" + if report_id == "ir": + item_ids = counter_identifiers(document) + yop = f"{key.get('publication_year') or 1:04d}" + + parent_data_type = "" + parent_title = "" + parent_ids = () + + if report_id == "ir" and source: + parent_data_type = key.get("parent_data_type") or PARENT_DATA_TYPES.get( + platform.collection_type, + "", + ) + if parent_data_type: + parent_title = source.get("title") or "" + parent_ids = counter_identifiers(source) + + return { + "name": _item_name(report_id, platform, source, document), + "publisher": "" if report_id == "pr" else _publisher(source), + "platform": platform.acron3, + "data_type": data_type, + "access_type": key.get("access_type") or "Open", + "access_method": key.get("access_method") or "Regular", + "metric_type": metric_type, + "yop": yop, + "source_ids": source_ids, + "item_ids": item_ids, + "source_key": key.get("source_key") or "", + "document_key": key.get("document_key") or "", + "parent_data_type": parent_data_type, + "parent_title": parent_title, + "parent_ids": parent_ids, + } + + +def _data_type(report_id, platform, key): + if report_id.startswith("tr"): + if platform.collection_type == "journals": + return "Journal" + return "Book" + if report_id == "pr": + if platform.collection_type == "journals": + return "Journal" + if platform.collection_type == "books": + return "Book" + + return key.get("data_type") or "Unspecified" + + +def _item_name(report_id, platform, source, document): + if report_id == "pr": + return platform.acron3 + if report_id.startswith("tr"): + return source.get("title") or source.get("source_id") or "Unknown" + + return document.get("title") or document.get("document_id") or "Unknown" + + +def _publisher(source): + value = source.get("publisher_name") or [] + if isinstance(value, list): + return "; ".join(str(item) for item in value) + + return str(value) + + +def _hide_attributes(entry, shown_attributes): + entry = dict(entry) + + for attribute, field in ATTRIBUTE_FIELDS.items(): + if attribute not in shown_attributes: + entry[field] = "" + + return entry + + +def _identity(report_id, entry): + excluded = {"source_key", "document_key"} + if report_id.startswith("tr"): + excluded.remove("source_key") + if report_id == "ir": + excluded.remove("source_key") + excluded.remove("document_key") + + return tuple((key, value) for key, value in entry.items() if key not in excluded) + + +def _month_value(value): + if isinstance(value, str): + return value[:7] + + return value.strftime("%Y-%m") + + +def _shown_attributes(report_id, params): + standard_view = STANDARD_VIEWS.get(report_id) or {} + attributes = set( + standard_view.get("attributes") or params.get("Attributes_To_Show") or [] + ) + if report_id in {"pr", "tr", "ir"}: + attributes.add("Data_Type") + + return attributes From dfe6f5788b0572f12b086397745fd8f346bc8cf2 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:39 -0300 Subject: [PATCH 22/47] =?UTF-8?q?Organiza=20t=C3=ADtulos=20e=20itens=20dos?= =?UTF-8?q?=20relat=C3=B3rios=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Serializa atributos, desempenho mensal e relações entre itens e seus periódicos ou livros. --- counter_api/report_items.py | 156 ++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 counter_api/report_items.py diff --git a/counter_api/report_items.py b/counter_api/report_items.py new file mode 100644 index 00000000..8f7283fb --- /dev/null +++ b/counter_api/report_items.py @@ -0,0 +1,156 @@ +from time import monotonic + +from counter_api.exceptions import service_unavailable +from counter_api.report_entries import build_entries + + +def build_report_items(report_id, platform, begin, records, params, deadline=None): + entries, exceptions = build_entries( + report_id, + platform, + records, + params, + deadline, + ) + items = _serialize_entries( + report_id, + entries, + begin, + params, + deadline, + ) + + return items, exceptions + + +def iter_report_items(items): + for item in items: + if "Items" in item: + for nested in item["Items"]: + yield item, nested + else: + yield {}, item + + +def _serialize_entries(report_id, entries, begin, params, deadline): + grouped = {} + + for entry in entries: + if deadline and monotonic() >= deadline: + raise service_unavailable() + + identifiers = entry["source_ids"] or entry["item_ids"] + item_key = ( + entry["name"], + entry["publisher"], + entry["platform"], + identifiers, + ) + if report_id.startswith("tr"): + item_key += (entry["source_key"],) + if report_id == "ir": + item_key += (entry["document_key"],) + if params.get("Include_Parent_Details"): + item_key += (entry["source_key"],) + + item = grouped.setdefault( + item_key, + { + "name": entry["name"], + "publisher": entry["publisher"], + "platform": entry["platform"], + "identifiers": identifiers, + "source_key": entry.get("source_key", ""), + "parent_data_type": entry.get("parent_data_type", ""), + "parent_title": entry.get("parent_title", ""), + "parent_ids": entry.get("parent_ids", ()), + "attributes": {}, + }, + ) + + attribute_key = ( + entry["data_type"], + entry["yop"], + entry["access_type"], + entry["access_method"], + ) + attributes = item["attributes"].setdefault(attribute_key, {}) + performance = attributes.setdefault(entry["metric_type"], {}) + monthly = entry["performance"] + + if params.get("Exclude_Monthly_Details"): + performance[begin.strftime("%Y-%m")] = sum(monthly.values()) + else: + performance.update(monthly) + + items = [] + parents = {} + parentless = [] + + for item in grouped.values(): + if deadline and monotonic() >= deadline: + raise service_unavailable() + + report_item = { + _name_field(report_id): item["name"], + "Platform": item["platform"], + "Attribute_Performance": [], + } + if report_id != "pr": + report_item["Publisher"] = item["publisher"] + if item["identifiers"]: + report_item["Item_ID"] = dict(item["identifiers"]) + + for attribute_key, performance in item["attributes"].items(): + data_type, yop, access_type, access_method = attribute_key + attribute_performance = {"Performance": performance} + + if data_type: + attribute_performance["Data_Type"] = data_type + if yop: + attribute_performance["YOP"] = yop + if access_type: + attribute_performance["Access_Type"] = access_type + if access_method: + attribute_performance["Access_Method"] = access_method + + report_item["Attribute_Performance"].append(attribute_performance) + + if report_id == "ir" and params.get("Include_Parent_Details"): + if item["source_key"] and item["parent_data_type"]: + parent = parents.setdefault( + item["source_key"], + { + "Title": item["parent_title"], + "Data_Type": item["parent_data_type"], + "Item_ID": dict(item["parent_ids"]), + "Items": [], + }, + ) + parent["Items"].append(report_item) + else: + parentless.append(report_item) + else: + items.append(report_item) + + if report_id == "ir" and params.get("Include_Parent_Details"): + items = list(parents.values()) + if parentless: + items.append({"Items": parentless}) + + if report_id == "ir": + if items and not params.get("Include_Parent_Details"): + items = [{"Items": sorted(items, key=lambda item: item["Item"])}] + + return sorted(items, key=lambda item: item.get("Title", "")) + + return sorted(items, key=lambda item: item[_name_field(report_id)]) + + +def _name_field(report_id): + if report_id == "pr": + return "Platform" + if report_id == "ir": + return "Item" + + return "Title" From b8d6c3995083dfb3b4941ccb6535bc5aa5116420 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:39 -0300 Subject: [PATCH 23/47] =?UTF-8?q?Monta=20cabe=C3=A7alhos=20e=20resultados?= =?UTF-8?q?=20dos=20relat=C3=B3rios=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combina itens, filtros, atributos e exceções de disponibilidade nas estruturas da Release 5.1. --- counter_api/reports.py | 145 +++++++++++ counter_api/tests/test_reports.py | 409 ++++++++++++++++++++++++++++++ 2 files changed, 554 insertions(+) create mode 100644 counter_api/reports.py create mode 100644 counter_api/tests/test_reports.py diff --git a/counter_api/reports.py b/counter_api/reports.py new file mode 100644 index 00000000..086a9164 --- /dev/null +++ b/counter_api/reports.py @@ -0,0 +1,145 @@ +from datetime import datetime, timezone + +from django.conf import settings + +from counter_api.constants import PROPRIETARY_NAMESPACE, RELEASE, REPORTS +from counter_api.parameters import report_parameters +from counter_api.report_items import build_report_items + + +def build_report(report_id, platform, begin, end, records, params, deadline=None): + definition = REPORTS[report_id] + params = report_parameters(report_id, params) + report_items, metadata_exceptions = build_report_items( + report_id, + platform, + begin, + records, + params, + deadline, + ) + + exceptions = list(params.get("parameter_exceptions") or []) + exceptions.extend(metadata_exceptions) + exceptions.extend( + _availability_exceptions( + params.get("pending_months"), + params.get("unavailable_months"), + report_items, + exceptions, + ) + ) + + if report_id == "pr": + exceptions.append( + { + "Code": 3040, + "Message": "Partial Data Returned", + "Data": "Searches_Platform is not measured", + } + ) + + header = _report_header( + report_id, + definition, + platform, + begin, + end, + params, + exceptions, + ) + + return { + "Report_Header": header, + "Report_Items": report_items, + } + + +def _report_header(report_id, definition, platform, begin, end, params, exceptions): + attributes = _report_attributes(params) + header = { + "Report_Name": definition["name"], + "Report_ID": report_id.upper(), + "Release": RELEASE, + "Institution_Name": settings.COUNTER_INSTITUTION_NAME, + "Institution_ID": { + "Proprietary": [f"{PROPRIETARY_NAMESPACE}:{settings.COUNTER_CUSTOMER_ID}"] + }, + "Report_Filters": _report_filters(platform, begin, end, params), + "Created": datetime.now(timezone.utc) + .isoformat(timespec="seconds") + .replace("+00:00", "Z"), + "Created_By": settings.COUNTER_CREATED_BY, + "Registry_Record": settings.COUNTER_REGISTRY_URL, + } + + if attributes: + header["Report_Attributes"] = attributes + if exceptions: + header["Exceptions"] = exceptions + + return header + + +def _report_filters(platform, begin, end, params): + filters = { + "Begin_Date": begin.isoformat(), + "End_Date": end.isoformat(), + "Platform": platform.acron3, + } + + for name in ("Data_Type", "Access_Type", "Access_Method", "Metric_Type", "YOP"): + if params.get(name): + filters[name] = params[name] + + if params.get("Item_ID"): + filters["Item_ID"] = params["Item_ID"] + + return filters + + +def _report_attributes(params): + attributes = {} + + if params.get("Attributes_To_Show"): + attributes["Attributes_To_Show"] = params["Attributes_To_Show"] + if params.get("Exclude_Monthly_Details"): + attributes["Granularity"] = "Totals" + if params.get("Include_Parent_Details"): + attributes["Include_Parent_Details"] = "True" + + return attributes + + +def _availability_exceptions(pending_months, unavailable_months, items, existing): + exceptions = [] + + if pending_months: + months = "|".join(month.strftime("%Y-%m") for month in pending_months) + exceptions.append( + { + "Code": 3031, + "Message": "Usage Not Ready for Requested Dates", + "Data": months, + } + ) + + if unavailable_months: + months = "|".join(month.strftime("%Y-%m") for month in unavailable_months) + exceptions.append( + { + "Code": 3032, + "Message": "Usage No Longer Available for Requested Dates", + "Data": months, + } + ) + + if not items and not exceptions and not existing: + exceptions.append( + { + "Code": 3030, + "Message": "No Usage Available for Requested Dates", + } + ) + + return exceptions diff --git a/counter_api/tests/test_reports.py b/counter_api/tests/test_reports.py new file mode 100644 index 00000000..81aaed4e --- /dev/null +++ b/counter_api/tests/test_reports.py @@ -0,0 +1,409 @@ +from datetime import date +from unittest.mock import Mock + +from django.test import TestCase +from openpyxl import load_workbook + +from counter_api.reports import build_report +from counter_api.tabular import build_workbook +from counter_api.tests.schema import validate_report + + +class ReportSemanticsTests(TestCase): + def test_empty_item_report_has_no_empty_group(self): + platform = Mock( + acron3="books", + main_name="SciELO Livros", + collection_type="books", + ) + + report = build_report( + "ir", + platform, + date(2024, 1, 1), + date(2024, 1, 31), + [], + {}, + ) + + self.assertEqual(report["Report_Items"], []) + validate_report(report, "ir") + + def test_item_report_groups_parent_and_adds_tabular_parent_columns(self): + platform = Mock( + acron3="scl", + main_name="SciELO Brasil", + collection_type="journals", + ) + bucket = { + "key": { + "source_key": "journal-1", + "document_key": "article-1", + "month": "2024-01", + "metric_scope": "item", + "data_type": "Article", + "parent_data_type": "Journal", + "access_type": "Open", + "access_method": "Regular", + "publication_year": 2020, + }, + "total_requests": {"value": 3}, + } + source = { + "title": "Journal", + "identifiers": {"print_issn": "1234-5678"}, + } + document = {"title": "Article", "identifiers": {"doi": "10.1234/article"}} + + report = build_report( + "ir", + platform, + date(2024, 1, 1), + date(2024, 1, 31), + [(bucket, source, document)], + {"Include_Parent_Details": True}, + ) + parent = report["Report_Items"][0] + + self.assertEqual(parent["Title"], "Journal") + self.assertEqual(parent["Data_Type"], "Journal") + self.assertEqual(parent["Item_ID"]["Print_ISSN"], "1234-5678") + self.assertEqual(parent["Items"][0]["Item"], "Article") + self.assertEqual( + report["Report_Header"]["Report_Attributes"]["Include_Parent_Details"], + "True", + ) + validate_report(report, "ir") + + workbook = build_workbook(report) + sheet = load_workbook(workbook, read_only=True).active + headings = [cell.value for cell in sheet[15]] + self.assertEqual(sheet["K16"].value, "Journal") + self.assertIn("Parent_Print_ISSN", headings) + self.assertEqual(sheet["S16"].value, "1234-5678") + + def test_global_item_report_for_journal_collection_uses_article_metadata(self): + platform = Mock( + acron3="scl", + main_name="SciELO Brasil", + collection_type="journals", + ) + rows = [ + { + "key": { + "source_key": "journal-1", + "document_key": "article-1", + "month": "2024-01", + "metric_scope": "item", + "data_type": "Article", + "access_type": "Open", + "access_method": "Regular", + "publication_year": 2020, + }, + "total_requests": {"value": 3}, + "total_investigations": {"value": 4}, + "unique_requests": {"value": 2}, + "unique_investigations": {"value": 2}, + } + ] + + report = build_report( + "ir", + platform, + date(2024, 1, 1), + date(2024, 1, 31), + [ + ( + rows[0], + {"title": "Journal"}, + { + "title": "Article", + "identifiers": {"doi": "10.1234/article"}, + }, + ) + ], + {}, + ) + item = report["Report_Items"][0]["Items"][0] + + self.assertEqual(item["Item"], "Article") + self.assertEqual(item["Item_ID"]["DOI"], "10.1234/article") + self.assertEqual(item["Attribute_Performance"][0]["Data_Type"], "Article") + validate_report(report, "ir") + + def test_book_item_report_excludes_title_scope_metrics(self): + platform = Mock( + acron3="books", + main_name="SciELO Livros", + collection_type="books", + ) + common = { + "source_key": "book-1", + "document_key": "chapter-1", + "month": "2024-01", + "data_type": "Book_Segment", + "access_type": "Open", + "access_method": "Regular", + "publication_year": 2020, + } + rows = [ + { + "key": {**common, "metric_scope": scope}, + "total_requests": {"value": 3}, + "unique_requests": {"value": 2}, + } + for scope in ("item", "title") + ] + + report = build_report( + "ir", + platform, + date(2024, 1, 1), + date(2024, 1, 31), + ( + ( + row, + {"title": "Book"}, + { + "title": "Chapter", + "identifiers": { + "book_id": "book-1", + "chapter_id": "chapter-1", + "pid_generic": "BOOK:BOOK-1/CHAPTER:CHAPTER-1", + }, + }, + ) + for row in rows + ), + {}, + ) + item = report["Report_Items"][0]["Items"][0] + performance = item["Attribute_Performance"][0]["Performance"] + + self.assertEqual(performance["Total_Item_Requests"]["2024-01"], 3) + self.assertNotIn("Unique_Title_Requests", performance) + self.assertEqual( + item["Item_ID"]["Proprietary"], + "BOOK:BOOK-1/CHAPTER:CHAPTER-1", + ) + + def test_platform_report_aggregates_sources(self): + platform = Mock( + acron3="scl", + main_name="SciELO Brasil", + collection_type="journals", + ) + rows = [] + for source_key in ("source-1", "source-2"): + rows.append( + { + "key": { + "source_key": source_key, + "document_key": f"document-{source_key}", + "month": "2024-01", + "metric_scope": "item", + "data_type": "Article", + "access_type": "Open", + "access_method": "Regular", + "publication_year": 2020, + }, + "total_requests": {"value": 1}, + "total_investigations": {"value": 1}, + "unique_requests": {"value": 1}, + "unique_investigations": {"value": 1}, + } + ) + + report = build_report( + "pr", + platform, + date(2024, 1, 1), + date(2024, 1, 31), + ( + ( + row, + { + "publisher_name": [ + "Publisher A" if index == 0 else "Publisher B" + ] + }, + {}, + ) + for index, row in enumerate(rows) + ), + {}, + ) + performance = report["Report_Items"][0]["Attribute_Performance"][0][ + "Performance" + ] + + self.assertEqual(len(report["Report_Items"]), 1) + self.assertEqual(performance["Total_Item_Requests"]["2024-01"], 2) + self.assertEqual(report["Report_Header"]["Exceptions"][0]["Code"], 3040) + self.assertEqual(report["Report_Items"][0]["Platform"], "scl") + self.assertRegex( + report["Report_Header"]["Created"], + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$", + ) + validate_report(report, "pr") + + def test_book_title_metrics_are_not_counted_as_item_metrics(self): + platform = Mock( + acron3="books", + main_name="SciELO Livros", + collection_type="books", + ) + common = { + "source_key": "source-1", + "document_key": "document-1", + "month": "2024-01", + "data_type": "Book", + "access_type": "Open", + "access_method": "Regular", + "publication_year": 2020, + } + rows = [ + { + "key": {**common, "metric_scope": "item"}, + "total_requests": {"value": 3}, + "total_investigations": {"value": 4}, + "unique_requests": {"value": 2}, + "unique_investigations": {"value": 2}, + }, + { + "key": {**common, "metric_scope": "title"}, + "total_requests": {"value": 3}, + "total_investigations": {"value": 4}, + "unique_requests": {"value": 0}, + "unique_investigations": {"value": 1}, + }, + ] + + report = build_report( + "tr_b3", + platform, + date(2024, 1, 1), + date(2024, 1, 31), + ( + ( + row, + { + "title": "Book", + "identifiers": { + "doi": "https://doi.org/10.1234/example", + "isbn": "8575410326", + }, + }, + {}, + ) + for row in rows + ), + {}, + ) + performance = report["Report_Items"][0]["Attribute_Performance"][0][ + "Performance" + ] + + self.assertEqual(performance["Total_Item_Requests"]["2024-01"], 3) + self.assertNotIn("Unique_Title_Requests", performance) + self.assertEqual(report["Report_Items"][0]["Item_ID"]["DOI"], "10.1234/example") + self.assertEqual( + report["Report_Items"][0]["Item_ID"]["ISBN"], + "978-85-7541-032-5", + ) + self.assertNotIn("Exceptions", report["Report_Header"]) + validate_report(report, "tr_b3") + + def test_title_report_omits_zero_metric_types(self): + platform = Mock( + acron3="books", + main_name="SciELO Livros", + collection_type="books", + ) + common = { + "source_key": "source-1", + "month": "2024-01", + "data_type": "Book", + "access_type": "Open", + "access_method": "Regular", + "publication_year": 2020, + } + title_bucket = { + "key": { + **common, + "metric_scope": "title", + }, + "unique_investigations": {"value": 1}, + "unique_requests": {"value": 0}, + } + item_bucket = { + "key": { + **common, + "metric_scope": "item", + }, + "total_investigations": {"value": 1}, + "unique_investigations": {"value": 1}, + } + + report = build_report( + "tr", + platform, + date(2024, 1, 1), + date(2024, 1, 31), + [ + (item_bucket, {"title": "Book"}, {}), + (title_bucket, {"title": "Book"}, {}), + ], + {}, + ) + performance = report["Report_Items"][0]["Attribute_Performance"][0][ + "Performance" + ] + + self.assertNotIn("Unique_Title_Requests", performance) + validate_report(report, "tr") + + def test_book_report_preserves_inconsistent_title_metrics_for_diagnosis(self): + platform = Mock( + acron3="books", + main_name="SciELO Livros", + collection_type="books", + ) + common = { + "source_key": "source-1", + "month": "2024-01", + "data_type": "Book", + "access_type": "Open", + "access_method": "Regular", + "publication_year": 2020, + } + rows = [ + { + "key": {**common, "metric_scope": "item"}, + "total_investigations": {"value": 3}, + "unique_investigations": {"value": 2}, + "total_requests": {"value": 3}, + "unique_requests": {"value": 3}, + }, + { + "key": {**common, "metric_scope": "title"}, + "unique_investigations": {"value": 4}, + "unique_requests": {"value": 3}, + }, + ] + + report = build_report( + "tr_b3", + platform, + date(2024, 1, 1), + date(2024, 1, 31), + ((row, {"title": "Book"}, {}) for row in rows), + {}, + ) + performance = report["Report_Items"][0]["Attribute_Performance"][0][ + "Performance" + ] + + self.assertEqual(performance["Unique_Title_Investigations"]["2024-01"], 4) + self.assertEqual(performance["Unique_Title_Requests"]["2024-01"], 3) + validate_report(report, "tr_b3") From e11ccbcc9a35fcfdc8be29d6a4ab6570e8e62230 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:29:39 -0300 Subject: [PATCH 24/47] =?UTF-8?q?Protege=20a=20integridade=20das=20m=C3=A9?= =?UTF-8?q?tricas=20de=20livros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Impede a publicação de relações impossíveis e da ausência de métricas únicas de título exigidas pelos relatórios de livros. --- counter_api/integrity.py | 70 +++++++++++++++++++++++++++++ counter_api/tests/test_integrity.py | 36 +++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 counter_api/integrity.py create mode 100644 counter_api/tests/test_integrity.py diff --git a/counter_api/integrity.py b/counter_api/integrity.py new file mode 100644 index 00000000..d4bf697f --- /dev/null +++ b/counter_api/integrity.py @@ -0,0 +1,70 @@ +from counter_api.report_items import iter_report_items + +METRIC_INVARIANTS = ( + ("Total_Item_Investigations", "Unique_Item_Investigations"), + ("Total_Item_Requests", "Unique_Item_Requests"), + ("Total_Item_Investigations", "Total_Item_Requests"), + ("Unique_Item_Investigations", "Unique_Item_Requests"), + ("Total_Item_Investigations", "Unique_Title_Investigations"), + ("Unique_Item_Investigations", "Unique_Title_Investigations"), + ("Total_Item_Requests", "Unique_Title_Requests"), + ("Unique_Item_Requests", "Unique_Title_Requests"), + ("Unique_Title_Investigations", "Unique_Title_Requests"), +) +BOOK_TITLE_REQUIREMENTS = ( + ( + "Unique_Title_Investigations", + ("Total_Item_Investigations", "Unique_Item_Investigations"), + ), + ( + "Unique_Title_Requests", + ("Total_Item_Requests", "Unique_Item_Requests"), + ), +) + + +def metric_invariant_errors(payload): + errors = [] + header = payload["Report_Header"] + report_id = header["Report_ID"] + selected_metrics = header["Report_Filters"].get("Metric_Type") + + for _, item in iter_report_items(payload["Report_Items"]): + name = item.get("Title") or item.get("Item") or item.get("Platform") or "" + + for attributes in item.get("Attribute_Performance", []): + performance = attributes.get("Performance", {}) + + for greater_metric, lesser_metric in METRIC_INVARIANTS: + greater = performance.get(greater_metric, {}) + lesser = performance.get(lesser_metric, {}) + + for month in set(greater) | set(lesser): + if lesser.get(month, 0) > greater.get(month, 0): + errors.append( + f"{name} {month}: {lesser_metric} exceeds {greater_metric}" + ) + + if report_id == "IR" or attributes.get("Data_Type") not in { + "Book", + "Reference_Work", + }: + continue + + for title_metric, item_metrics in BOOK_TITLE_REQUIREMENTS: + if selected_metrics and title_metric not in selected_metrics: + continue + + months = { + month + for metric in item_metrics + for month in performance.get(metric, {}) + } + + for month in months: + if performance.get(title_metric, {}).get(month, 0): + continue + + errors.append(f"{name} {month}: {title_metric} is missing") + + return errors diff --git a/counter_api/tests/test_integrity.py b/counter_api/tests/test_integrity.py new file mode 100644 index 00000000..434288eb --- /dev/null +++ b/counter_api/tests/test_integrity.py @@ -0,0 +1,36 @@ +from unittest import TestCase + +from counter_api.integrity import metric_invariant_errors + + +class ReportIntegrityTests(TestCase): + def test_book_reports_require_title_metrics_alongside_item_metrics(self): + payload = { + "Report_Header": { + "Report_ID": "TR_B3", + "Report_Filters": { + "Metric_Type": [ + "Total_Item_Investigations", + "Unique_Title_Investigations", + ] + }, + }, + "Report_Items": [ + { + "Title": "Book", + "Attribute_Performance": [ + { + "Data_Type": "Book", + "Performance": { + "Total_Item_Investigations": {"2024-01": 2}, + }, + } + ], + } + ], + } + + self.assertEqual( + metric_invariant_errors(payload), + ["Book 2024-01: Unique_Title_Investigations is missing"], + ) From ea9ac8f949d86d41f9bad6a0e73aa3720a02b3b5 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:10 -0300 Subject: [PATCH 25/47] =?UTF-8?q?Orquestra=20a=20gera=C3=A7=C3=A3o=20dos?= =?UTF-8?q?=20relat=C3=B3rios=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combina cobertura, parâmetros, consultas, montagem e verificação de integridade em um fluxo único. --- counter_api/generation.py | 54 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 counter_api/generation.py diff --git a/counter_api/generation.py b/counter_api/generation.py new file mode 100644 index 00000000..65c1f631 --- /dev/null +++ b/counter_api/generation.py @@ -0,0 +1,54 @@ +from counter_api.availability import split_period +from counter_api.exceptions import CounterAPIError, service_unavailable +from counter_api.integrity import metric_invariant_errors +from counter_api.parameters import parse_parameters, report_parameters +from counter_api.query import ReportQuery +from counter_api.reports import build_report + + +def generate_report(report_id, platform, begin, end, query_params): + months, pending, unavailable = split_period(platform, begin, end) + params = parse_parameters(query_params, report_id) + params["pending_months"] = pending + params["unavailable_months"] = unavailable + + report_params = report_parameters(report_id, params) + query = ReportQuery() + + if params.get("Item_ID"): + try: + item_field, item_key = query.resolve_item_filter( + platform, + report_id, + params["Item_ID"], + ) + report_params["Item_Key_Field"] = item_field + report_params["Item_Key"] = item_key + except CounterAPIError as error: + if error.code not in {3060, 3061}: + raise + params["parameter_exceptions"].append(error.as_dict()) + + records = query.fetch( + platform, + months, + report_id, + report_params, + ) + + payload = build_report( + report_id, + platform, + begin, + end, + records, + params, + deadline=query.deadline, + ) + + if platform.collection_type == "books" and report_id in {"pr", "tr", "tr_b3"}: + errors = metric_invariant_errors(payload) + if errors: + raise service_unavailable(f"Book metrics inconsistent: {errors[0]}") + + return payload, query.deadline From ccb320e96e2a528c45ea6ea22371d19be4496ea4 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:10 -0300 Subject: [PATCH 26/47] =?UTF-8?q?Gera=20relat=C3=B3rios=20COUNTER=20em=20f?= =?UTF-8?q?ormatos=20tabulares?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Produz as mesmas linhas em TSV e XLSX, respeita limites de planilha e reutiliza a iteração mensal comum. --- counter_api/negotiation.py | 11 ++ counter_api/tabular.py | 300 +++++++++++++++++++++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 counter_api/negotiation.py create mode 100644 counter_api/tabular.py diff --git a/counter_api/negotiation.py b/counter_api/negotiation.py new file mode 100644 index 00000000..182a3193 --- /dev/null +++ b/counter_api/negotiation.py @@ -0,0 +1,11 @@ +from rest_framework.negotiation import BaseContentNegotiation + + +class CounterContentNegotiation(BaseContentNegotiation): + def select_parser(self, request, parsers): + return None + + def select_renderer(self, request, renderers, format_suffix=None): + renderer = renderers[0] + + return renderer, renderer.media_type diff --git a/counter_api/tabular.py b/counter_api/tabular.py new file mode 100644 index 00000000..0ef24270 --- /dev/null +++ b/counter_api/tabular.py @@ -0,0 +1,300 @@ +import csv +from codecs import BOM_UTF8 +from datetime import date +from io import StringIO +from tempfile import SpooledTemporaryFile +from time import monotonic + +from openpyxl import Workbook +from openpyxl.cell import WriteOnlyCell +from openpyxl.styles import Font + +from counter_api.constants import EXCEL_MAX_ROWS, SPOOL_SIZE, STANDARD_VIEWS +from counter_api.dates import iter_months +from counter_api.exceptions import insufficient_information, service_unavailable +from counter_api.report_items import iter_report_items + +MONTH_LABELS = ( + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +) +TABULAR_FILTER_EXCLUSIONS = {"Begin_Date", "End_Date", "Metric_Type"} + + +def build_workbook(report, deadline=None): + workbook = Workbook(write_only=True) + sheet = workbook.create_sheet() + sheet.title = report["Report_Header"]["Report_ID"] + for column in (1, 2, 4): + sheet.column_dimensions[chr(64 + column)].width = 28 + + for row_number, row in enumerate(_report_rows(report), start=1): + if deadline and monotonic() >= deadline: + workbook.close() + raise service_unavailable() + + if row_number > EXCEL_MAX_ROWS: + workbook.close() + raise insufficient_information() + + if row_number == 15: + heading = [] + for value in row: + cell = WriteOnlyCell(sheet, value=value) + cell.font = Font(bold=True) + heading.append(cell) + row = heading + + sheet.append(row) + + output = SpooledTemporaryFile(max_size=SPOOL_SIZE) + if deadline and monotonic() >= deadline: + workbook.close() + output.close() + raise service_unavailable() + + workbook.save(output) + output.seek(0) + + return output + + +def build_tsv(report, deadline=None): + output = SpooledTemporaryFile(max_size=SPOOL_SIZE) + output.write(BOM_UTF8) + line = StringIO(newline="") + writer = csv.writer(line, delimiter="\t", lineterminator="\n") + + for row in _report_rows(report): + if deadline and monotonic() >= deadline: + output.close() + raise service_unavailable() + + writer.writerow(row) + output.write(line.getvalue().encode("utf-8")) + line.seek(0) + line.truncate(0) + + output.seek(0) + + return output + + +def _report_rows(report): + header = report["Report_Header"] + yield from ( + ("Report_Name", header["Report_Name"]), + ("Report_ID", header["Report_ID"]), + ("Release", header["Release"]), + ("Institution_Name", header["Institution_Name"]), + ("Institution_ID", _header_identifiers(header["Institution_ID"])), + ("Metric_Types", "; ".join(_metric_types(report))), + ( + "Report_Filters", + _pairs( + { + key: value + for key, value in header["Report_Filters"].items() + if key not in TABULAR_FILTER_EXCLUSIONS + } + ), + ), + ("Report_Attributes", _pairs(header.get("Report_Attributes") or {})), + ("Exceptions", _exceptions(header.get("Exceptions") or [])), + ("Reporting_Period", _reporting_period(header["Report_Filters"])), + ("Created", header["Created"]), + ("Created_By", header["Created_By"]), + ("Registry_Record", header.get("Registry_Record", "")), + ("", ""), + ) + + months = _months(header["Report_Filters"]) + columns = _columns(header, months) + yield [ + f"{MONTH_LABELS[int(column[5:]) - 1]}-{column[:4]}" + if column in months + else column + for column in columns + ] + + for values in _item_values(report["Report_Items"]): + yield [values.get(column, "") for column in columns] + + +def _item_values(items): + for parent, item in iter_report_items(items): + identifiers = item.get("Item_ID") or {} + parent_ids = parent.get("Item_ID") or {} + common = { + "Platform": item.get("Platform", ""), + "Title": item.get("Title", ""), + "Item": item.get("Item", ""), + "Publisher": item.get("Publisher", ""), + "Publisher_ID": "", + "DOI": identifiers.get("DOI", ""), + "Proprietary_ID": identifiers.get("Proprietary", ""), + "ISBN": identifiers.get("ISBN", ""), + "Print_ISSN": identifiers.get("Print_ISSN", ""), + "Online_ISSN": identifiers.get("Online_ISSN", ""), + "URI": identifiers.get("URI", ""), + "Parent_Title": parent.get("Title", ""), + "Parent_Authors": "", + "Parent_Publication_Date": "", + "Parent_Article_Version": "", + "Parent_Data_Type": parent.get("Data_Type", ""), + "Parent_DOI": parent_ids.get("DOI", ""), + "Parent_Proprietary_ID": parent_ids.get("Proprietary", ""), + "Parent_ISBN": parent_ids.get("ISBN", ""), + "Parent_Print_ISSN": parent_ids.get("Print_ISSN", ""), + "Parent_Online_ISSN": parent_ids.get("Online_ISSN", ""), + "Parent_URI": parent_ids.get("URI", ""), + } + + for attributes in item.get("Attribute_Performance", []): + for metric_type, counts in attributes.get("Performance", {}).items(): + values = dict(common) + values.update( + { + "Data_Type": attributes.get("Data_Type", ""), + "YOP": attributes.get("YOP", ""), + "Access_Type": attributes.get("Access_Type", ""), + "Access_Method": attributes.get("Access_Method", ""), + "Metric_Type": metric_type, + "Reporting_Period_Total": sum(counts.values()), + **counts, + } + ) + yield values + + +def _months(filters): + begin = date.fromisoformat(filters["Begin_Date"]).replace(day=1) + end = date.fromisoformat(filters["End_Date"]).replace(day=1) + + return [month.strftime("%Y-%m") for month in iter_months(begin, end)] + + +def _header_identifiers(values): + identifiers = [] + for namespace, namespace_values in values.items(): + if namespace == "Proprietary": + identifiers.extend(namespace_values) + else: + identifiers.extend(f"{namespace}:{value}" for value in namespace_values) + return "; ".join(identifiers) + + +def _metric_types(report): + metrics = set() + for _, item in iter_report_items(report["Report_Items"]): + for attributes in item.get("Attribute_Performance", []): + metrics.update(attributes.get("Performance", {})) + return sorted(metrics) + + +def _columns(header, months): + report_id = header["Report_ID"] + standard_view = STANDARD_VIEWS.get(report_id.lower()) or {} + attributes = list( + standard_view.get("attributes") + or (header.get("Report_Attributes") or {}).get("Attributes_To_Show", []) + ) + if report_id in {"PR", "TR", "IR"} and "Data_Type" not in attributes: + attributes.insert(0, "Data_Type") + metric_columns = [ + *attributes, + "Metric_Type", + "Reporting_Period_Total", + *months, + ] + if report_id == "PR": + return ["Platform", *metric_columns] + if report_id == "IR": + parent_columns = [] + if (header.get("Report_Attributes") or {}).get("Include_Parent_Details"): + parent_columns = [ + "Parent_Title", + "Parent_Authors", + "Parent_Publication_Date", + "Parent_Article_Version", + "Parent_Data_Type", + "Parent_DOI", + "Parent_Proprietary_ID", + "Parent_ISBN", + "Parent_Print_ISSN", + "Parent_Online_ISSN", + "Parent_URI", + ] + return [ + "Item", + "Publisher", + "Publisher_ID", + "Platform", + "DOI", + "Proprietary_ID", + "ISBN", + "Print_ISSN", + "Online_ISSN", + "URI", + *parent_columns, + *metric_columns, + ] + if report_id == "TR_J3": + return [ + "Title", + "Publisher", + "Publisher_ID", + "Platform", + "DOI", + "Proprietary_ID", + "Print_ISSN", + "Online_ISSN", + "URI", + *metric_columns, + ] + return [ + "Title", + "Publisher", + "Publisher_ID", + "Platform", + "DOI", + "Proprietary_ID", + "ISBN", + "Print_ISSN", + "Online_ISSN", + "URI", + *metric_columns, + ] + + +def _pairs(values): + pairs = [] + for key, value in values.items(): + if isinstance(value, list): + value = "|".join(value) + pairs.append(f"{key}={value}") + return "; ".join(pairs) + + +def _exceptions(values): + result = [] + for value in values: + exception = f"{value['Code']}: {value['Message']}" + if value.get("Data"): + exception += f" ({value['Data']})" + result.append(exception) + return "; ".join(result) + + +def _reporting_period(filters): + return f"Begin_Date={filters['Begin_Date']}; " f"End_Date={filters['End_Date']}" From a37fffef8a49857acc4e4d8b4a07c7f8d1028462 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:10 -0300 Subject: [PATCH 27/47] Descreve os contratos HTTP da interface COUNTER MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Define parâmetros, respostas e estruturas da Release 5.1 para a documentação automática. --- counter_api/openapi.py | 222 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 counter_api/openapi.py diff --git a/counter_api/openapi.py b/counter_api/openapi.py new file mode 100644 index 00000000..0006b2a1 --- /dev/null +++ b/counter_api/openapi.py @@ -0,0 +1,222 @@ +from django.conf import settings +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse +from rest_framework import serializers + +from counter_api.constants import REPORTS + +EXCEL_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" +COUNTER_AUTH = [{"CounterAPIKey": []}] + + +class ExceptionSerializer(serializers.Serializer): + Code = serializers.IntegerField() + Message = serializers.CharField() + Data = serializers.CharField(required=False) + + +class StatusSerializer(serializers.Serializer): + Service_Active = serializers.BooleanField() + Note = serializers.CharField() + Alerts = serializers.ListField(child=serializers.DictField()) + Registry_URL = serializers.URLField(required=False) + + +class MemberSerializer(serializers.Serializer): + Customer_ID = serializers.CharField() + Institution_Name = serializers.CharField() + Institution_ID = serializers.DictField() + + +class PlatformSerializer(serializers.Serializer): + Platform_Parameter = serializers.CharField() + Platform_Name = serializers.CharField() + Platform_Description = serializers.CharField() + + +class ReportDescriptorSerializer(serializers.Serializer): + Report_ID = serializers.CharField() + Release = serializers.CharField() + Report_Name = serializers.CharField() + Report_Description = serializers.CharField() + Path = serializers.CharField() + First_Month_Available = serializers.CharField() + Last_Month_Available = serializers.CharField() + + +class ReportHeaderSerializer(serializers.Serializer): + Report_Name = serializers.CharField() + Report_ID = serializers.CharField() + Release = serializers.CharField() + Institution_Name = serializers.CharField() + Institution_ID = serializers.DictField() + Report_Filters = serializers.DictField() + Report_Attributes = serializers.DictField(required=False) + Exceptions = ExceptionSerializer(many=True, required=False) + Created = serializers.DateTimeField() + Created_By = serializers.CharField() + Registry_Record = serializers.URLField(required=False) + + +class ReportItemSerializer(serializers.Serializer): + Platform = serializers.CharField() + Title = serializers.CharField(required=False) + Item = serializers.CharField(required=False) + Publisher = serializers.CharField(required=False) + Item_ID = serializers.DictField(required=False) + Attribute_Performance = serializers.ListField(child=serializers.DictField()) + + +class CounterReportSerializer(serializers.Serializer): + Report_Header = ReportHeaderSerializer() + Report_Items = ReportItemSerializer(many=True) + + +CUSTOMER_PARAMETER = OpenApiParameter( + name="customer_id", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + description=( + "Institution identifier. Current public customer: " + f"{settings.COUNTER_CUSTOMER_ID}." + ), +) +PLATFORM_PARAMETER = OpenApiParameter( + name="platform", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=True, + description="Platform parameter returned by /r51/platforms, for example scl.", +) +BEGIN_DATE_PARAMETER = OpenApiParameter( + name="begin_date", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=True, + description="First reporting month in YYYY-MM or first-day YYYY-MM-DD format.", +) +END_DATE_PARAMETER = OpenApiParameter( + name="end_date", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=True, + description="Last reporting month in YYYY-MM or last-day YYYY-MM-DD format.", +) +REPORT_ID_PARAMETER = OpenApiParameter( + name="report_id", + type=OpenApiTypes.STR, + location=OpenApiParameter.PATH, + required=True, + enum=list(REPORTS), + description="COUNTER report identifier.", +) +FORMAT_PARAMETER = OpenApiParameter( + name="format", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + enum=["json", "xlsx", "tsv"], + description="Response format. Defaults to json.", +) +ATTRIBUTES_PARAMETER = OpenApiParameter( + name="attributes_to_show", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + description=( + "Pipe-separated attributes: Data_Type, YOP, Access_Type, Access_Method." + ), +) +DATA_TYPE_PARAMETER = OpenApiParameter( + name="data_type", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + description="Pipe-separated COUNTER data types.", +) +ACCESS_TYPE_PARAMETER = OpenApiParameter( + name="access_type", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + description="Pipe-separated access types.", +) +ACCESS_METHOD_PARAMETER = OpenApiParameter( + name="access_method", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + description="Pipe-separated access methods.", +) +METRIC_TYPE_PARAMETER = OpenApiParameter( + name="metric_type", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + description="Pipe-separated COUNTER metric types.", +) +YOP_PARAMETER = OpenApiParameter( + name="yop", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + description="Pipe-separated publication years or ranges.", +) +ITEM_ID_PARAMETER = OpenApiParameter( + name="item_id", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + description=( + "Exact item identifier for TR or IR, such as DOI:10.1234/example, " + "Print_ISSN:1234-5678 or Proprietary:identifier." + ), +) +EXCLUDE_MONTHLY_PARAMETER = OpenApiParameter( + name="exclude_monthly_details", + type=OpenApiTypes.BOOL, + location=OpenApiParameter.QUERY, + required=False, +) +INCLUDE_PARENT_PARAMETER = OpenApiParameter( + name="include_parent_details", + type=OpenApiTypes.BOOL, + location=OpenApiParameter.QUERY, + required=False, + description="For IR, group items under their journal or book parent.", +) + +MEMBER_PARAMETERS = [CUSTOMER_PARAMETER] +REPORT_CATALOG_PARAMETERS = [ + CUSTOMER_PARAMETER, + PLATFORM_PARAMETER, +] +REPORT_PARAMETERS = [ + CUSTOMER_PARAMETER, + PLATFORM_PARAMETER, + BEGIN_DATE_PARAMETER, + END_DATE_PARAMETER, + REPORT_ID_PARAMETER, + FORMAT_PARAMETER, + ATTRIBUTES_PARAMETER, + DATA_TYPE_PARAMETER, + ACCESS_TYPE_PARAMETER, + ACCESS_METHOD_PARAMETER, + METRIC_TYPE_PARAMETER, + YOP_PARAMETER, + ITEM_ID_PARAMETER, + EXCLUDE_MONTHLY_PARAMETER, + INCLUDE_PARENT_PARAMETER, +] + +REPORT_RESPONSES = { + (200, "application/json"): CounterReportSerializer, + (200, EXCEL_CONTENT_TYPE): OpenApiTypes.BINARY, + (200, "text/tab-separated-values"): OpenApiTypes.BINARY, + 400: ExceptionSerializer, + 401: ExceptionSerializer, + 403: ExceptionSerializer, + 404: OpenApiResponse(description="Report unavailable for the platform."), + 503: ExceptionSerializer, +} From 143de0b5998b41e5aed23b62f87f215ebd8e3b58 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:10 -0300 Subject: [PATCH 28/47] Isola o acesso HTTP da interface COUNTER MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mantém autenticação por chave, respostas JSON e negociação compatível com downloads tabulares. --- counter_api/access.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 counter_api/access.py diff --git a/counter_api/access.py b/counter_api/access.py new file mode 100644 index 00000000..0b610b2b --- /dev/null +++ b/counter_api/access.py @@ -0,0 +1,25 @@ +from rest_framework.renderers import JSONRenderer +from rest_framework.response import Response +from rest_framework.views import APIView + +from counter_api.exceptions import CounterAPIError +from counter_api.models import APIKey +from counter_api.negotiation import CounterContentNegotiation + + +class CounterAPIView(APIView): + authentication_classes = [] + permission_classes = [] + renderer_classes = [JSONRenderer] + content_negotiation_class = CounterContentNegotiation + http_method_names = ["get"] + + +def get_authentication_error(request): + key = APIKey.authenticate(request.query_params.get("api_key")) + if key: + return None + + error = CounterAPIError(2020, "APIKey Invalid", 401) + + return Response(error.as_dict(), status=error.status_code) From d219e6c004358be3cbe80a158c5e0b7d1273ab38 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:10 -0300 Subject: [PATCH 29/47] =?UTF-8?q?Exp=C3=B5e=20os=20recursos=20HTTP=20da=20?= =?UTF-8?q?interface=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Publica descoberta e geração de relatórios, incluindo downloads, limites de plataforma e tratamento de falhas. --- counter_api/tests/test_report_views.py | 262 +++++++++++++++++++++++++ counter_api/views.py | 228 +++++++++++++++++++++ 2 files changed, 490 insertions(+) create mode 100644 counter_api/tests/test_report_views.py create mode 100644 counter_api/views.py diff --git a/counter_api/tests/test_report_views.py b/counter_api/tests/test_report_views.py new file mode 100644 index 00000000..6510938d --- /dev/null +++ b/counter_api/tests/test_report_views.py @@ -0,0 +1,262 @@ +from datetime import date +from io import BytesIO +from unittest.mock import patch + +from django.test import TestCase +from openpyxl import load_workbook + +from collection.models import Collection +from counter_api.models import APIKey +from counter_api.openapi import EXCEL_CONTENT_TYPE +from counter_api.reports import build_report +from counter_api.tabular import build_tsv +from counter_api.tests.schema import validate_report + + +class ReportViewTests(TestCase): + def setUp(self): + self.platform = Collection.objects.create( + acron3="scl", + main_name="SciELO Brasil", + collection_type="journals", + is_active=True, + ) + _key, self.api_key = APIKey.issue("tests") + + def test_title_report_omits_facts_without_source_metadata(self): + records = [ + ( + { + "key": { + "source_key": "missing-source", + "month": "2025-08", + "metric_scope": "item", + "data_type": "Article", + "access_type": "Open", + "access_method": "Regular", + }, + "total_requests": {"value": 1}, + "total_investigations": {"value": 1}, + "unique_requests": {"value": 1}, + "unique_investigations": {"value": 1}, + }, + {}, + {}, + ) + ] + report = build_report( + "tr_j3", + self.platform, + date(2025, 8, 1), + date(2025, 8, 31), + records, + {}, + ) + + self.assertEqual(report["Report_Items"], []) + self.assertEqual( + report["Report_Header"]["Exceptions"], + [ + { + "Code": 3040, + "Message": "Partial Data Returned", + "Data": "Metadata missing for 1 title(s)", + } + ], + ) + + def test_title_report_includes_required_data_type(self): + bucket = { + "key": { + "source_key": "journal-1", + "document_key": "article-1", + "month": "2025-08", + "metric_scope": "item", + "data_type": "Article", + "access_type": "Open", + "access_method": "Regular", + "publication_year": 2025, + }, + "total_investigations": {"value": 1}, + "total_requests": {"value": 1}, + } + source = {"title": "Journal"} + + report = build_report( + "tr", + self.platform, + date(2025, 8, 1), + date(2025, 8, 31), + [(bucket, source, {})], + {}, + ) + attributes = report["Report_Items"][0]["Attribute_Performance"][0] + tsv = build_tsv(report) + lines = tsv.read().decode("utf-8-sig").splitlines() + tsv.close() + + self.assertEqual(attributes["Data_Type"], "Journal") + self.assertIn("Data_Type", lines[14].split("\t")) + validate_report(report, "tr") + + @patch("counter_api.generation.split_period") + @patch("counter_api.generation.ReportQuery") + def test_book_report_rejects_inconsistent_index_metrics( + self, query_class, split_period + ): + Collection.objects.create( + acron3="books", + main_name="SciELO Livros", + collection_type="books", + is_active=True, + ) + split_period.return_value = ([date(2025, 8, 1)], [], []) + query_class.return_value.deadline = float("inf") + query_class.return_value.fetch.return_value = [ + ( + { + "key": { + "month": "2025-08", + "metric_scope": "item", + "access_type": "Open", + "access_method": "Regular", + }, + "total_investigations": {"value": 2}, + "unique_investigations": {"value": 2}, + }, + {}, + {}, + ) + ] + + response = self.client.get( + "/r51/reports/pr", + { + "api_key": self.api_key, + "platform": "books", + "begin_date": "2025-08", + "end_date": "2025-08", + }, + ) + + self.assertEqual(response.status_code, 503) + self.assertEqual(response.json()["Code"], 1000) + self.assertIn("Unique_Title_Investigations is missing", response.json()["Data"]) + + @patch("counter_api.generation.split_period") + @patch("counter_api.generation.ReportQuery") + def test_report_uses_r51_shape_and_downloads_xlsx(self, query_class, split_period): + split_period.return_value = ( + [date(2024, 1, 1), date(2024, 2, 1)], + [], + [], + ) + query_class.return_value.fetch.return_value = [ + ( + { + "key": { + "source_key": "source-1", + "document_key": "document-1", + "month": "2024-01", + "metric_scope": "item", + "data_type": "Article", + "access_type": "Open", + "access_method": "Regular", + "publication_year": 2023, + }, + "total_requests": {"value": 4}, + "total_investigations": {"value": 5}, + "unique_requests": {"value": 2}, + "unique_investigations": {"value": 3}, + }, + { + "title": "Journal", + "publisher_name": ["SciELO"], + "identifiers": {"print_issn": "1234-5678"}, + }, + {}, + ) + ] + query_class.return_value.deadline = float("inf") + params = { + "api_key": self.api_key, + "platform": "scl", + "begin_date": "2024-01", + "end_date": "2024-02", + } + + response = self.client.get("/r51/reports/tr_j3", params) + payload = response.json() + item = payload["Report_Items"][0] + + self.assertEqual(response.status_code, 200) + self.assertIn("Attribute_Performance", item) + self.assertNotIn("Performance", item) + self.assertEqual( + query_class.return_value.fetch.call_args.args[1], + [date(2024, 1, 1), date(2024, 2, 1)], + ) + self.assertEqual( + payload["Report_Header"]["Report_Filters"]["Data_Type"], + ["Journal"], + ) + self.assertEqual( + set(item["Attribute_Performance"][0]), + {"Access_Type", "Performance"}, + ) + self.assertEqual( + item["Attribute_Performance"][0]["Performance"][ + "Total_Item_Investigations" + ]["2024-01"], + 5, + ) + self.assertNotIn( + "2024-02", + item["Attribute_Performance"][0]["Performance"][ + "Total_Item_Investigations" + ], + ) + validate_report(payload, "tr_j3") + + params["format"] = "invalid" + response = self.client.get("/r51/reports/tr_j3", params) + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json()["Report_Header"]["Exceptions"][0]["Code"], 3060 + ) + + params["format"] = "xlsx" + for accept in ("application/json", EXCEL_CONTENT_TYPE): + response = self.client.get( + "/r51/reports/tr_j3", + params, + HTTP_ACCEPT=accept, + ) + workbook = load_workbook( + BytesIO(b"".join(response.streaming_content)), + read_only=True, + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], EXCEL_CONTENT_TYPE) + self.assertEqual( + response["Content-Disposition"], + 'attachment; filename="tr_j3_scl_202401_202402.xlsx"', + ) + self.assertEqual(workbook.active["A1"].value, "Report_Name") + self.assertEqual(workbook.active["A15"].value, "Title") + self.assertEqual(workbook.active["G15"].value, "Print_ISSN") + self.assertEqual(workbook.active["G16"].value, "1234-5678") + self.assertNotIn("Begin_Date", workbook.active["B7"].value) + + params["format"] = "tsv" + response = self.client.get("/r51/reports/tr_j3", params) + lines = b"".join(response.streaming_content).decode("utf-8").splitlines() + + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "text/tab-separated-values") + self.assertIn("Total_Item_Investigations", lines[5]) + self.assertNotIn("Metric_Type", lines[6]) + self.assertEqual(lines[14].split("\t")[-2:], ["Jan-2024", "Feb-2024"]) + self.assertEqual(lines[15].split("\t")[6], "1234-5678") diff --git a/counter_api/views.py b/counter_api/views.py new file mode 100644 index 00000000..5d090114 --- /dev/null +++ b/counter_api/views.py @@ -0,0 +1,228 @@ +import logging + +from django.conf import settings +from django.http import FileResponse +from drf_spectacular.utils import extend_schema +from rest_framework.response import Response + +from counter_api.access import CounterAPIView, get_authentication_error +from counter_api.availability import available_months +from counter_api.constants import PROPRIETARY_NAMESPACE, RELEASE, REPORTS +from counter_api.dates import parse_period +from counter_api.exceptions import CounterAPIError, service_unavailable +from counter_api.generation import generate_report +from counter_api.openapi import ( + COUNTER_AUTH, + EXCEL_CONTENT_TYPE, + MEMBER_PARAMETERS, + REPORT_CATALOG_PARAMETERS, + REPORT_PARAMETERS, + REPORT_RESPONSES, + ExceptionSerializer, + MemberSerializer, + PlatformSerializer, + ReportDescriptorSerializer, + StatusSerializer, +) +from counter_api.parameters import validate_customer +from counter_api.platforms import get_platform, list_platforms +from counter_api.tabular import build_tsv, build_workbook + + +class StatusView(CounterAPIView): + @extend_schema( + tags=["Service"], + auth=[], + operation_id="counter_status", + summary="Check COUNTER service status", + responses={200: StatusSerializer}, + ) + def get(self, request): + payload = { + "Service_Active": True, + "Note": "SciELO COUNTER API", + "Alerts": [], + } + registry_url = getattr(settings, "COUNTER_REGISTRY_URL", "") + if registry_url: + payload["Registry_URL"] = registry_url + + return Response(payload) + + +class MembersView(CounterAPIView): + @extend_schema( + tags=["Discovery"], + auth=COUNTER_AUTH, + operation_id="counter_members", + summary="List COUNTER customers", + parameters=MEMBER_PARAMETERS, + responses={ + 200: MemberSerializer(many=True), + 401: ExceptionSerializer, + 403: ExceptionSerializer, + }, + ) + def get(self, request): + authentication_error = get_authentication_error(request) + if authentication_error: + return authentication_error + + try: + validate_customer(request.query_params) + except CounterAPIError as error: + return Response(error.as_dict(), status=error.status_code) + + payload = [ + { + "Customer_ID": settings.COUNTER_CUSTOMER_ID, + "Institution_Name": settings.COUNTER_INSTITUTION_NAME, + "Institution_ID": { + "Proprietary": [ + f"{PROPRIETARY_NAMESPACE}:{settings.COUNTER_CUSTOMER_ID}" + ] + }, + } + ] + + return Response(payload) + + +class PlatformsView(CounterAPIView): + @extend_schema( + tags=["Discovery"], + auth=COUNTER_AUTH, + operation_id="counter_platforms", + summary="List SciELO COUNTER platforms", + responses={ + 200: PlatformSerializer(many=True), + 401: ExceptionSerializer, + }, + ) + def get(self, request): + authentication_error = get_authentication_error(request) + if authentication_error: + return authentication_error + + return Response(list_platforms()) + + +class ReportsView(CounterAPIView): + @extend_schema( + tags=["Discovery"], + auth=COUNTER_AUTH, + operation_id="counter_reports", + summary="List reports available for a platform", + parameters=REPORT_CATALOG_PARAMETERS, + responses={ + 200: ReportDescriptorSerializer(many=True), + 400: ExceptionSerializer, + 401: ExceptionSerializer, + 403: ExceptionSerializer, + 503: ExceptionSerializer, + }, + ) + def get(self, request): + authentication_error = get_authentication_error(request) + if authentication_error: + return authentication_error + + try: + validate_customer(request.query_params) + platform = get_platform(request.query_params.get("platform")) + except CounterAPIError as error: + return Response(error.as_dict(), status=error.status_code) + + try: + months = available_months(platform) + except CounterAPIError as error: + return Response(error.as_dict(), status=error.status_code) + if not months: + return Response([]) + + first_month = months[0].strftime("%Y-%m") + last_month = months[-1].strftime("%Y-%m") + payload = [] + + for report_id, definition in REPORTS.items(): + if platform.collection_type not in definition["collections"]: + continue + payload.append( + { + "Report_ID": report_id.upper(), + "Release": RELEASE, + "Report_Name": definition["name"], + "Report_Description": definition["name"], + "Path": f"/r51/reports/{report_id}", + "First_Month_Available": first_month, + "Last_Month_Available": last_month, + } + ) + + return Response(payload) + + +class ReportView(CounterAPIView): + @extend_schema( + tags=["Reports"], + auth=COUNTER_AUTH, + operation_id="counter_report", + summary="Generate a COUNTER R5.1 report", + description=( + "Returns COUNTER JSON by default. Set format=xlsx or format=tsv " + "for a tabular download. PR omits Searches_Platform and " + "reports COUNTER exception 3040." + ), + parameters=REPORT_PARAMETERS, + responses=REPORT_RESPONSES, + ) + def get(self, request, report_id): + authentication_error = get_authentication_error(request) + if authentication_error: + return authentication_error + + try: + validate_customer(request.query_params) + platform = get_platform(request.query_params.get("platform")) + definition = REPORTS.get(report_id) + if ( + not definition + or platform.collection_type not in definition["collections"] + ): + return Response({}, status=404) + + begin, end = parse_period(request.query_params) + payload, deadline = generate_report( + report_id, + platform, + begin, + end, + request.query_params, + ) + + output_format = request.query_params.get("format") + if output_format in {"xlsx", "tsv"}: + if output_format == "xlsx": + content = build_workbook(payload, deadline=deadline) + content_type = EXCEL_CONTENT_TYPE + else: + content = build_tsv(payload, deadline=deadline) + content_type = "text/tab-separated-values" + + response = FileResponse(content, content_type=content_type) + filename = ( + f"{report_id}_{platform.acron3}_{begin:%Y%m}_{end:%Y%m}." + f"{output_format}" + ) + response["Content-Disposition"] = f'attachment; filename="{filename}"' + + return response + except CounterAPIError as error: + return Response(error.as_dict(), status=error.status_code) + except Exception: + logging.exception("COUNTER report generation failed") + exception = service_unavailable() + + return Response(exception.as_dict(), status=exception.status_code) + + return Response(payload) From b1b392901e310b9e7e7a6421dd9d1ebfa712ce3d Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:10 -0300 Subject: [PATCH 30/47] Remove chaves COUNTER dos eventos do Sentry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elimina a consulta de eventos e transações antes do envio quando ela contém a chave de acesso. --- config/sentry.py | 28 ++++++++++++++++++++++++++++ config/settings/production.py | 8 +++++++- counter_api/tests/test_sentry.py | 21 +++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 config/sentry.py create mode 100644 counter_api/tests/test_sentry.py diff --git a/config/sentry.py b/config/sentry.py new file mode 100644 index 00000000..a44b7201 --- /dev/null +++ b/config/sentry.py @@ -0,0 +1,28 @@ +from urllib.parse import parse_qsl, urlsplit, urlunsplit + + +def scrub_api_key(event, _hint): + request = event.get("request") or {} + query_string = request.get("query_string") or "" + url = request.get("url") or "" + url_query = urlsplit(url).query if url else "" + + if not _contains_api_key(query_string) and not _contains_api_key(url_query): + return event + + request["query_string"] = "" + if url: + parts = urlsplit(url) + request["url"] = urlunsplit((parts.scheme, parts.netloc, parts.path, "", "")) + + return event + + +def _contains_api_key(query_string): + if not isinstance(query_string, str): + return False + + return any( + name.lower() == "api_key" + for name, _value in parse_qsl(query_string, keep_blank_values=True) + ) diff --git a/config/settings/production.py b/config/settings/production.py index 373fa251..a17e1222 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -6,6 +6,8 @@ from sentry_sdk.integrations.logging import LoggingIntegration from sentry_sdk.integrations.redis import RedisIntegration +from config.sentry import scrub_api_key + from .base import * # noqa from .base import env @@ -86,7 +88,9 @@ EMAIL_PORT = env.int("DJANGO_EMAIL_PORT", default=25) # https://docs.djangoproject.com/en/dev/ref/settings/#email-host-user -EMAIL_HOST_USER = env.str("DJANGO_EMAIL_HOST_USER", default="suporte.aplicacao@scielo.org") +EMAIL_HOST_USER = env.str( + "DJANGO_EMAIL_HOST_USER", default="suporte.aplicacao@scielo.org" +) # https://docs.djangoproject.com/en/dev/ref/settings/#std-setting-EMAIL_HOST_PASSWORD EMAIL_HOST_PASSWORD = env.str("DJANGO_EMAIL_HOST_PASSWORD", default="") @@ -197,6 +201,8 @@ integrations=integrations, environment=env("SENTRY_ENVIRONMENT", default="production"), traces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.0), + before_send=scrub_api_key, + before_send_transaction=scrub_api_key, ) # Redis diff --git a/counter_api/tests/test_sentry.py b/counter_api/tests/test_sentry.py new file mode 100644 index 00000000..17701f36 --- /dev/null +++ b/counter_api/tests/test_sentry.py @@ -0,0 +1,21 @@ +from django.test import SimpleTestCase + +from config.sentry import scrub_api_key + + +class SentryTests(SimpleTestCase): + def test_removes_api_key_from_error_and_transaction_events(self): + event = { + "request": { + "url": "https://example.org/r51/reports/tr?api_key=secret&platform=scl", + "query_string": "api_key=secret&platform=scl", + } + } + + result = scrub_api_key(event, {}) + + self.assertEqual(result["request"]["query_string"], "") + self.assertEqual( + result["request"]["url"], + "https://example.org/r51/reports/tr", + ) From 27aa36198c7c1ada428b5ef07f55ed739a79afca Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:11 -0300 Subject: [PATCH 31/47] =?UTF-8?q?Adiciona=20sincroniza=C3=A7=C3=A3o=20dos?= =?UTF-8?q?=20metadados=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permite reconstruir os índices de fontes e documentos usados pelos relatórios e extensões. --- .../commands/sync_counter_metadata.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 counter_api/management/commands/sync_counter_metadata.py diff --git a/counter_api/management/commands/sync_counter_metadata.py b/counter_api/management/commands/sync_counter_metadata.py new file mode 100644 index 00000000..fdf1b6a7 --- /dev/null +++ b/counter_api/management/commands/sync_counter_metadata.py @@ -0,0 +1,20 @@ +from django.core.management.base import BaseCommand + +from metrics.services.metadata_sync import DEFAULT_BATCH_SIZE, sync_metadata + + +class Command(BaseCommand): + help = "Sync COUNTER source and document metadata to OpenSearch." + + def add_arguments(self, parser): + parser.add_argument("--batch-size", type=int, default=DEFAULT_BATCH_SIZE) + parser.add_argument("--full", action="store_true") + + def handle(self, *args, **options): + batch_size = options["batch_size"] + if batch_size <= 0: + raise ValueError("batch-size must be greater than zero") + + result = sync_metadata(batch_size=batch_size, full=options["full"]) + + self.stdout.write(" ".join(f"{name}={value}" for name, value in result.items())) From 5b0eb6d9f9cefbc45c0a04a2f2745c47d1f6b256 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:11 -0300 Subject: [PATCH 32/47] =?UTF-8?q?Adiciona=20preenchimento=20do=20ano=20de?= =?UTF-8?q?=20publica=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Atualiza a dimensão de ano nos índices mensais a partir do índice separado de documentos. --- .../commands/backfill_counter_yop.py | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 counter_api/management/commands/backfill_counter_yop.py diff --git a/counter_api/management/commands/backfill_counter_yop.py b/counter_api/management/commands/backfill_counter_yop.py new file mode 100644 index 00000000..f8b424bb --- /dev/null +++ b/counter_api/management/commands/backfill_counter_yop.py @@ -0,0 +1,127 @@ +from datetime import date +from itertools import islice + +from django.conf import settings +from django.core.management.base import BaseCommand +from opensearchpy import helpers + +from collection.models import Collection +from metrics.opensearch.client import OpenSearchUsageClient +from metrics.opensearch.names import generate_metadata_alias, generate_month_index_name + +DEFAULT_BATCH_SIZE = 500 +MAX_PUBLICATION_YEAR = 9999 + + +class Command(BaseCommand): + help = "Add the YOP mapping and backfill monthly COUNTER facts." + + def add_arguments(self, parser): + parser.add_argument("--collection", required=True) + parser.add_argument("--start-month", required=True) + parser.add_argument("--end-month", required=True) + parser.add_argument("--batch-size", type=int, default=DEFAULT_BATCH_SIZE) + parser.add_argument("--dry-run", action="store_true") + + def handle(self, *args, **options): + collection = Collection.objects.get(acron3=options["collection"]) + batch_size = options["batch_size"] + if batch_size <= 0: + raise ValueError("batch-size must be greater than zero") + + try: + start_month = date.fromisoformat(f"{options['start_month']}-01") + end_month = date.fromisoformat(f"{options['end_month']}-01") + except ValueError: + raise ValueError("months must use YYYY-MM") + + if start_month > end_month: + raise ValueError("start-month must not be later than end-month") + + client = OpenSearchUsageClient().client + facts_alias = generate_month_index_name( + settings.OPENSEARCH_INDEX_NAME, + collection.acron3, + ) + documents_alias = generate_metadata_alias( + settings.OPENSEARCH_INDEX_NAME, + "documents", + ) + + if not options["dry_run"]: + indexes = client.indices.get_alias(name=facts_alias) + for index_name in indexes: + client.indices.put_mapping( + index=index_name, + body={"properties": {"publication_year": {"type": "integer"}}}, + ) + + query = { + "query": { + "range": { + "month": { + "gte": options["start_month"], + "lte": options["end_month"], + } + } + }, + "_source": ["document_key", "publication_year"], + } + hits = helpers.scan(client, index=facts_alias, query=query) + totals = { + "updated": 0, + "correct": 0, + "missing_metadata": 0, + "invalid_yop": 0, + } + + while True: + batch = list(islice(hits, batch_size)) + if not batch: + break + + keys = sorted( + { + hit["_source"].get("document_key") + for hit in batch + if hit["_source"].get("document_key") + } + ) + metadata = client.mget(index=documents_alias, body={"ids": keys}) + years = { + document["_id"]: document["_source"].get("publication_year") + for document in metadata.get("docs", []) + if document.get("found") + } + + actions = [] + + for hit in batch: + document_key = hit["_source"].get("document_key") + if document_key not in years: + totals["missing_metadata"] += 1 + continue + + year = years[document_key] + if not isinstance(year, int) or year < 1 or year > MAX_PUBLICATION_YEAR: + totals["invalid_yop"] += 1 + year = 1 + + if hit["_source"].get("publication_year") == year: + totals["correct"] += 1 + continue + + actions.append( + { + "_op_type": "update", + "_index": hit["_index"], + "_id": hit["_id"], + "doc": {"publication_year": year}, + } + ) + + totals["updated"] += len(actions) + if actions and not options["dry_run"]: + helpers.bulk(client, actions, chunk_size=batch_size) + + self.stdout.write(" ".join(f"{name}={value}" for name, value in totals.items())) From b201aa1b7fd0e222dad82478f2f9b2b6df4f1787 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:11 -0300 Subject: [PATCH 33/47] =?UTF-8?q?Atualiza=20a=20identifica=C3=A7=C3=A3o=20?= =?UTF-8?q?da=20lista=20de=20rob=C3=B4s=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adota o nome atual da fonte oficial exibida na administração de agentes automatizados. --- resources/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/models.py b/resources/models.py index 8021d6f3..76313bb1 100644 --- a/resources/models.py +++ b/resources/models.py @@ -84,7 +84,7 @@ def get_patterns(cls, source=None): def source_labels(self): labels = [] if self.source_counter: - labels.append("Atmire/COUNTER") + labels.append("COUNTER") if self.source_scielo: labels.append("SciELO") return ", ".join(labels) or "-" From 4cecd6c40871417a1bd808e2064796131232e2dd Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:45 -0300 Subject: [PATCH 34/47] =?UTF-8?q?Define=20contratos=20das=20extens=C3=B5es?= =?UTF-8?q?=20de=20uso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centraliza entidades, dimensões, formatos e a escolha entre índices mensais e anuais. --- counter_api/extensions/__init__.py | 0 counter_api/extensions/contracts.py | 95 +++++++++++++++++++++++++++++ counter_api/extensions/query.py | 49 +++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 counter_api/extensions/__init__.py create mode 100644 counter_api/extensions/contracts.py create mode 100644 counter_api/extensions/query.py diff --git a/counter_api/extensions/__init__.py b/counter_api/extensions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/counter_api/extensions/contracts.py b/counter_api/extensions/contracts.py new file mode 100644 index 00000000..bfaed560 --- /dev/null +++ b/counter_api/extensions/contracts.py @@ -0,0 +1,95 @@ +DOWNLOAD_FORMATS = ("json", "csv", "xlsx") +SEGMENT_DIMENSIONS = {"country", "language"} +RANKING_GROUPS = {"", "country", "language", "country,language"} +RANKING_ENTITIES = { + "journals": { + "collection_type": "journals", + "parent_type": "collection", + "key_field": "source_key", + "metadata": "sources", + }, + "books": { + "collection_type": "books", + "parent_type": "collection", + "key_field": "source_key", + "metadata": "sources", + }, + "articles": { + "collection_type": "journals", + "parent_type": "journal", + "key_field": "document_key", + "metadata": "documents", + "data_type": "Article", + }, + "chapters": { + "collection_type": "books", + "parent_type": "book", + "key_field": "document_key", + "metadata": "documents", + "data_type": "Book_Segment", + }, + "preprints": { + "collection_type": "preprints", + "parent_type": "collection", + "key_field": "document_key", + "metadata": "documents", + }, + "datasets": { + "collection_type": "data", + "parent_type": "collection", + "key_field": "document_key", + "metadata": "documents", + }, +} +DISTRIBUTION_DIMENSIONS = { + "country": "country_code", + "language": "content_language", + "yop": "publication_year", + "subject_area": "source_key", + "journal": "source_key", +} +DISTRIBUTION_CROSSES = ( + ("country", "language"), + ("country", "yop"), + ("language", "yop"), + ("journal", "language"), +) +DISTRIBUTION_ENTITIES = { + "collection": {}, + "journal": { + "collection_type": "journals", + "key_field": "source_key", + "report_id": "tr", + }, + "book": { + "collection_type": "books", + "key_field": "source_key", + "report_id": "tr", + }, + "article": { + "collection_type": "journals", + "key_field": "document_key", + "report_id": "ir", + "data_type": "Article", + }, + "chapter": { + "collection_type": "books", + "key_field": "document_key", + "report_id": "ir", + "data_type": "Book_Segment", + }, + "preprint": { + "collection_type": "preprints", + "key_field": "document_key", + "report_id": "ir", + }, + "dataset": { + "collection_type": "data", + "key_field": "document_key", + "report_id": "ir", + }, +} +DISTRIBUTION_CAPABILITIES = ( + *DISTRIBUTION_DIMENSIONS, + *(",".join(dimensions) for dimensions in DISTRIBUTION_CROSSES), +) diff --git a/counter_api/extensions/query.py b/counter_api/extensions/query.py new file mode 100644 index 00000000..f687c027 --- /dev/null +++ b/counter_api/extensions/query.py @@ -0,0 +1,49 @@ +from django.conf import settings + +from counter_api.dates import iter_months +from counter_api.exceptions import insufficient_information, invalid_dates +from counter_api.extensions.contracts import SEGMENT_DIMENSIONS +from metrics.opensearch.names import ( + generate_analytics_index_name, + generate_month_index_name, +) + + +def validate_segmented_period(begin, end, dimensions, metric_type): + segmented = bool(SEGMENT_DIMENSIONS & set(dimensions)) + if not segmented: + return False + if begin.month != 1 or end.month != 12: + raise invalid_dates() + if metric_type.startswith("Unique_"): + raise insufficient_information() + + return True + + +def usage_scope(platform, begin, end, segmented, data_type=None): + if segmented: + index_name = generate_analytics_index_name( + settings.OPENSEARCH_INDEX_NAME, + platform.acron3, + ) + periods = [str(year) for year in range(begin.year, end.year + 1)] + filters = [{"terms": {"year": periods}}] + else: + index_name = generate_month_index_name( + settings.OPENSEARCH_INDEX_NAME, + platform.acron3, + ) + periods = [month.strftime("%Y-%m") for month in iter_months(begin, end)] + filters = [{"terms": {"month": periods}}] + + filters.extend( + [ + {"term": {"metric_scope": "item"}}, + {"term": {"access_method": "Regular"}}, + ] + ) + if data_type: + filters.append({"term": {"data_type": data_type}}) + + return index_name, filters From b33e4a0f712aa1a39becb7d82a3411558f1818ca Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:45 -0300 Subject: [PATCH 35/47] =?UTF-8?q?Adiciona=20rankings=20=C3=A0s=20extens?= =?UTF-8?q?=C3=B5es=20de=20uso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calcula os itens mais acessados com paginação completa, desempate estável e filtros por metadados. --- counter_api/extensions/ranking.py | 361 +++++++++++++++++++ counter_api/tests/test_extensions_ranking.py | 218 +++++++++++ 2 files changed, 579 insertions(+) create mode 100644 counter_api/extensions/ranking.py create mode 100644 counter_api/tests/test_extensions_ranking.py diff --git a/counter_api/extensions/ranking.py b/counter_api/extensions/ranking.py new file mode 100644 index 00000000..2349c13c --- /dev/null +++ b/counter_api/extensions/ranking.py @@ -0,0 +1,361 @@ +from heapq import heappush, heappushpop +from time import monotonic + +from django.conf import settings + +from counter_api.constants import METADATA_BATCH_SIZE, METRICS +from counter_api.exceptions import insufficient_information +from counter_api.extensions.contracts import RANKING_ENTITIES, RANKING_GROUPS +from counter_api.extensions.query import usage_scope, validate_segmented_period +from counter_api.metadata import fetch_metadata +from counter_api.query import ReportQuery +from counter_api.search import composite_pages, search +from counter_api.yop import matches_yop, yop_filter +from metrics.opensearch.names import generate_metadata_alias + +RANKING_PAGE_SIZE = 1000 +DEFAULT_LIMIT = 100 +MAX_LIMIT = 1000 +MAX_GROUPS = 20 +MAX_RESULTS = 10000 +MAX_FILTER_KEYS = 10000 + + +class RankingQuery: + def __init__(self, client=None): + usage_client = client or ReportQuery() + self.client = usage_client.client + self.report_query = usage_client + + def run(self, platform, begin, end, params): + deadline = monotonic() + settings.COUNTER_QUERY_BUDGET_SECONDS + ( + relation, + parent_type, + parent_id, + limit, + group_by, + metric_type, + segmented, + ) = self._parameters(platform, begin, end, params) + parent_key = self._parent_key(platform, parent_type, parent_id) + key_batches = self._key_batches(platform, params, deadline) + groups = self._ranked_groups( + platform, + begin, + end, + relation, + params, + parent_key, + metric_type, + group_by, + key_batches, + limit, + deadline, + segmented, + ) + + return { + "platform": platform.acron3, + "begin_date": begin.isoformat(), + "end_date": end.isoformat(), + "parent_type": parent_type, + "parent_id": parent_id, + "entity_type": params["entity_type"], + "metric_type": metric_type, + "groups": self._result_groups(groups, relation, group_by, deadline), + } + + def _parameters(self, platform, begin, end, params): + entity_type = params.get("entity_type") + relation = RANKING_ENTITIES.get(entity_type) + parent_type = params.get("parent_type") + parent_id = params.get("parent_id") + + if not relation or platform.collection_type != relation["collection_type"]: + raise insufficient_information() + if parent_type != relation["parent_type"] or not parent_id: + raise insufficient_information() + + try: + limit = int(params.get("limit", DEFAULT_LIMIT)) + except (TypeError, ValueError): + raise insufficient_information() + + if limit < 1 or limit > MAX_LIMIT: + raise insufficient_information() + + group_by = params.get("group_by", "") + if group_by not in RANKING_GROUPS: + raise insufficient_information() + + metric_type = params.get("metric_type", "Total_Item_Requests") + if metric_type not in METRICS: + raise insufficient_information() + + if params.get("yop"): + values = [value for value in params["yop"].split("|") if value] + if not yop_filter(values): + raise insufficient_information() + + dimensions = set(group_by.split(",")) if group_by else set() + dimensions.update( + dimension for dimension in ("country", "language") if params.get(dimension) + ) + segmented = validate_segmented_period( + begin, + end, + dimensions, + metric_type, + ) + if segmented and params.get("yop") and relation["metadata"] != "documents": + raise insufficient_information() + + return ( + relation, + parent_type, + parent_id, + limit, + group_by, + metric_type, + segmented, + ) + + def _parent_key(self, platform, parent_type, parent_id): + if parent_type == "collection": + if parent_id != platform.acron3: + raise insufficient_information() + return None + + return self.report_query.resolve_item_id(platform, "tr", parent_id) + + def _key_batches(self, platform, params, deadline): + subject_area = params.get("subject_area") + if not subject_area: + return [None] + + source_keys = self._source_keys(platform, subject_area, deadline) + + return [ + source_keys[start : start + METADATA_BATCH_SIZE] + for start in range(0, len(source_keys), METADATA_BATCH_SIZE) + ] + + def _ranked_groups( + self, + platform, + begin, + end, + relation, + params, + parent_key, + metric_type, + group_by, + key_batches, + limit, + deadline, + segmented, + ): + groups = {} + + for source_keys in key_batches: + index_name, body = self._query_body( + platform, + begin, + end, + relation["key_field"], + relation.get("data_type"), + parent_key, + metric_type, + group_by, + params, + source_keys, + segmented, + ) + + for buckets in composite_pages(self.client, index_name, body, deadline): + yop_keys = self._yop_keys( + buckets, + relation["key_field"], + params, + deadline, + segmented, + ) + + self._collect_groups( + groups, + buckets, + relation["key_field"], + limit, + yop_keys, + ) + + return groups + + def _collect_groups(self, groups, buckets, entity_key, limit, yop_keys): + for bucket in buckets: + count = int(bucket["count"]["value"]) + key = bucket["key"][entity_key] + if count <= 0 or not key: + continue + if yop_keys is not None and key not in yop_keys: + continue + + group = tuple( + bucket["key"].get(dimension) + for dimension in ("country_code", "content_language") + if dimension in bucket["key"] + ) + winners = groups.setdefault(group, []) + if len(groups) > MAX_GROUPS or len(groups) * limit > MAX_RESULTS: + raise insufficient_information() + + inverted_key = bytes(255 - byte for byte in key.encode("utf-8")) + candidate = (count, inverted_key, key) + if len(winners) < limit: + heappush(winners, candidate) + elif candidate > winners[0]: + heappushpop(winners, candidate) + + def _yop_keys(self, buckets, entity_key, params, deadline, segmented): + if not segmented or not params.get("yop"): + return None + + values = [value for value in params["yop"].split("|") if value] + keys = [ + bucket["key"].get(entity_key) + for bucket in buckets + if bucket["key"].get(entity_key) + ] + metadata = fetch_metadata(self.client, "documents", keys, deadline) + + return { + key + for key in keys + if matches_yop(metadata.get(key, {}).get("publication_year"), values) + } + + def _result_groups(self, groups, relation, group_by, deadline): + winner_keys = {item[2] for winners in groups.values() for item in winners} + metadata = fetch_metadata( + self.client, + relation["metadata"], + winner_keys, + deadline, + ) + dimensions = [ + dimension for dimension in ("country", "language") if dimension in group_by + ] + result = [] + + for group, winners in sorted(groups.items(), key=lambda item: str(item[0])): + items = [] + for count, _, key in sorted(winners, key=lambda item: (-item[0], item[2])): + item = metadata.get(key, {}) + items.append( + { + "id": key, + "title": item.get("title") or "", + "count": count, + } + ) + + labels = [ + "Unknown" if not value or value == "_unknown" else value + for value in group + ] + result.append({"dimensions": dict(zip(dimensions, labels)), "items": items}) + + return result + + def _query_body( + self, + platform, + begin, + end, + entity_key, + data_type, + parent_key, + metric_type, + group_by, + params, + source_keys, + segmented, + ): + index_name, filters = usage_scope( + platform, + begin, + end, + segmented, + data_type, + ) + + if parent_key: + filters.append({"term": {"source_key": parent_key}}) + if params.get("country"): + filters.append({"term": {"country_code": params["country"]}}) + if params.get("language"): + filters.append({"term": {"content_language": params["language"]}}) + if source_keys: + filters.append({"terms": {"source_key": source_keys}}) + if params.get("yop") and not segmented: + values = [value for value in params["yop"].split("|") if value] + clause = yop_filter(values) + if not clause: + raise insufficient_information() + filters.append(clause) + + dimensions = [] + if "country" in group_by: + dimensions.append("country_code") + if "language" in group_by: + dimensions.append("content_language") + dimensions.append(entity_key) + + sources = [ + {field: {"terms": {"field": field, "missing_bucket": True}}} + for field in dimensions + ] + body = { + "size": 0, + "track_total_hits": False, + "query": {"bool": {"filter": filters}}, + "aggs": { + "rows": { + "composite": {"size": RANKING_PAGE_SIZE, "sources": sources}, + "aggs": {"count": {"sum": {"field": METRICS[metric_type]}}}, + } + }, + } + + return index_name, body + + def _source_keys(self, platform, subject_area, deadline): + index_name = generate_metadata_alias(settings.OPENSEARCH_INDEX_NAME, "sources") + body = { + "size": METADATA_BATCH_SIZE, + "_source": False, + "track_total_hits": False, + "sort": [{"_id": "asc"}], + "query": { + "bool": { + "filter": [ + {"term": {"collection": platform.acron3}}, + {"term": {"subject_areas": subject_area}}, + ] + } + }, + } + keys = [] + + while True: + response = search(self.client, index_name, body, deadline) + hits = response["hits"]["hits"] + keys.extend(hit["_id"] for hit in hits) + if len(keys) > MAX_FILTER_KEYS: + raise insufficient_information() + + if len(hits) < METADATA_BATCH_SIZE: + return keys + + body["search_after"] = hits[-1]["sort"] diff --git a/counter_api/tests/test_extensions_ranking.py b/counter_api/tests/test_extensions_ranking.py new file mode 100644 index 00000000..00bfee9f --- /dev/null +++ b/counter_api/tests/test_extensions_ranking.py @@ -0,0 +1,218 @@ +from datetime import date +from types import SimpleNamespace +from unittest import TestCase +from unittest.mock import Mock + +from counter_api.exceptions import CounterAPIError +from counter_api.extensions.ranking import RankingQuery + + +class RankingQueryTests(TestCase): + def test_ranking_reads_all_pages_and_breaks_ties_by_item_key(self): + client = Mock() + client.search.side_effect = [ + { + "aggregations": { + "rows": { + "buckets": [ + { + "key": { + "country_code": "BR", + "content_language": "pt", + "document_key": "item-b", + }, + "count": {"value": 10}, + } + ], + "after_key": {"document_key": "item-b"}, + } + } + }, + { + "aggregations": { + "rows": { + "buckets": [ + { + "key": { + "country_code": "BR", + "content_language": "pt", + "document_key": "item-a", + }, + "count": {"value": 10}, + } + ] + } + } + }, + ] + client.mget.return_value = { + "docs": [ + {"_id": "item-a", "found": True, "_source": {"title": "Article A"}} + ] + } + query = RankingQuery(Mock(client=client)) + platform = SimpleNamespace(acron3="scl", collection_type="journals") + + query.report_query.resolve_item_id.return_value = "journal-key" + result = query.run( + platform, + date(2025, 1, 1), + date(2025, 12, 31), + { + "entity_type": "articles", + "parent_type": "journal", + "parent_id": "Print_ISSN:1234-5678", + "group_by": "country,language", + "limit": "1", + }, + ) + + self.assertEqual( + result["groups"][0]["dimensions"], {"country": "BR", "language": "pt"} + ) + self.assertEqual( + result["groups"][0]["items"], + [{"id": "item-a", "title": "Article A", "count": 10}], + ) + self.assertEqual(client.search.call_count, 2) + body = client.search.call_args_list[0].kwargs["body"] + self.assertIn({"terms": {"year": ["2025"]}}, body["query"]["bool"]["filter"]) + self.assertIn( + {"term": {"source_key": "journal-key"}}, body["query"]["bool"]["filter"] + ) + + def test_country_ranking_requires_complete_year(self): + query = RankingQuery(Mock(client=Mock())) + platform = SimpleNamespace(acron3="scl", collection_type="journals") + + with self.assertRaises(CounterAPIError): + query.run( + platform, + date(2025, 1, 1), + date(2025, 6, 30), + { + "entity_type": "journals", + "parent_type": "collection", + "parent_id": "scl", + "group_by": "country", + }, + ) + + def test_subject_area_keys_and_yop_are_filtered_before_aggregation(self): + client = Mock() + client.search.side_effect = [ + {"hits": {"hits": [{"_id": "journal-1", "sort": ["journal-1"]}]}}, + { + "aggregations": { + "rows": { + "buckets": [ + { + "key": {"source_key": "journal-1"}, + "count": {"value": 8}, + } + ] + } + } + }, + ] + client.mget.return_value = { + "docs": [ + { + "_id": "journal-1", + "found": True, + "_source": {"title": "Journal"}, + } + ] + } + query = RankingQuery(Mock(client=client)) + platform = SimpleNamespace(acron3="scl", collection_type="journals") + + result = query.run( + platform, + date(2024, 1, 1), + date(2024, 12, 31), + { + "entity_type": "journals", + "parent_type": "collection", + "parent_id": "scl", + "subject_area": "Health", + "yop": "2020|2022-2023", + }, + ) + + self.assertEqual(result["groups"][0]["items"][0]["id"], "journal-1") + metadata_body = client.search.call_args_list[0].kwargs["body"] + self.assertIn( + {"term": {"subject_areas": "Health"}}, + metadata_body["query"]["bool"]["filter"], + ) + facts_body = client.search.call_args_list[1].kwargs["body"] + filters = facts_body["query"]["bool"]["filter"] + self.assertIn({"terms": {"source_key": ["journal-1"]}}, filters) + self.assertIn( + { + "bool": { + "should": [ + {"term": {"publication_year": 2020}}, + {"range": {"publication_year": {"gte": 2022, "lte": 2023}}}, + ], + "minimum_should_match": 1, + } + }, + filters, + ) + + def test_country_ranking_filters_yop_with_document_metadata(self): + client = Mock() + client.search.return_value = { + "aggregations": { + "rows": { + "buckets": [ + { + "key": {"country_code": "BR", "document_key": "old"}, + "count": {"value": 12}, + }, + { + "key": {"country_code": "BR", "document_key": "new"}, + "count": {"value": 8}, + }, + ] + } + } + } + client.mget.return_value = { + "docs": [ + { + "_id": "old", + "found": True, + "_source": {"title": "Old", "publication_year": 2019}, + }, + { + "_id": "new", + "found": True, + "_source": {"title": "New", "publication_year": 2022}, + }, + ] + } + query = RankingQuery(Mock(client=client)) + platform = SimpleNamespace(acron3="scl", collection_type="journals") + + result = query.run( + platform, + date(2024, 1, 1), + date(2024, 12, 31), + { + "entity_type": "articles", + "parent_type": "journal", + "parent_id": "Print_ISSN:1234-5678", + "group_by": "country", + "yop": "2020-2023", + }, + ) + + self.assertEqual( + result["groups"][0]["items"], + [{"id": "new", "title": "New", "count": 8}], + ) + filters = client.search.call_args.kwargs["body"]["query"]["bool"]["filter"] + self.assertFalse(any("publication_year" in str(item) for item in filters)) From 44f92867eec16ea5ca0600be5586b98e37526631 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:45 -0300 Subject: [PATCH 36/47] =?UTF-8?q?Adiciona=20distribui=C3=A7=C3=B5es=20?= =?UTF-8?q?=C3=A0s=20extens=C3=B5es=20de=20uso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agrega dimensões simples e cruzadas sem inflar totais de metadados multivalorados. --- counter_api/extensions/distribution.py | 259 ++++++++++++++++++ .../tests/test_extensions_distribution.py | 259 ++++++++++++++++++ 2 files changed, 518 insertions(+) create mode 100644 counter_api/extensions/distribution.py create mode 100644 counter_api/tests/test_extensions_distribution.py diff --git a/counter_api/extensions/distribution.py b/counter_api/extensions/distribution.py new file mode 100644 index 00000000..7af6636a --- /dev/null +++ b/counter_api/extensions/distribution.py @@ -0,0 +1,259 @@ +from time import monotonic + +from django.conf import settings + +from counter_api.constants import METRICS +from counter_api.exceptions import insufficient_information +from counter_api.extensions.contracts import ( + DISTRIBUTION_CROSSES, + DISTRIBUTION_DIMENSIONS, + DISTRIBUTION_ENTITIES, +) +from counter_api.extensions.query import usage_scope, validate_segmented_period +from counter_api.metadata import fetch_metadata +from counter_api.query import ReportQuery +from counter_api.search import composite_pages + +PAGE_SIZE = 1000 +MAX_BUCKETS = 1000 + + +class DistributionQuery: + def __init__(self, client=None): + usage_client = client or ReportQuery() + self.client = usage_client.client + self.report_query = usage_client + + def run(self, platform, begin, end, params): + deadline = monotonic() + settings.COUNTER_QUERY_BUDGET_SECONDS + dimensions, entity_type, entity_id, metric_type, segmented = self._parameters( + platform, + begin, + end, + params, + ) + key_field, entity_key = self._entity_filter( + platform, + entity_type, + entity_id, + ) + index_name, body = self._query_body( + platform, + begin, + end, + dimensions, + entity_type, + metric_type, + key_field, + entity_key, + segmented, + ) + total, buckets = self._buckets( + index_name, + body, + dimensions, + deadline, + ) + + return { + "platform": platform.acron3, + "begin_date": begin.isoformat(), + "end_date": end.isoformat(), + "entity_type": entity_type, + "entity_id": entity_id, + "dimension": ",".join(dimensions), + "dimensions": list(dimensions), + "metric_type": metric_type, + "total": total, + "buckets": buckets, + } + + def _parameters(self, platform, begin, end, params): + dimensions = tuple( + item.strip() for item in params.get("dimension", "").split(",") if item + ) + entity_type = params.get("entity_type") + entity_id = params.get("entity_id") + metric_type = params.get("metric_type", "Total_Item_Requests") + + if ( + not dimensions + or len(dimensions) > 2 + or any(dimension not in DISTRIBUTION_DIMENSIONS for dimension in dimensions) + or (len(dimensions) == 2 and dimensions not in DISTRIBUTION_CROSSES) + or metric_type not in METRICS + or entity_type not in DISTRIBUTION_ENTITIES + or not entity_id + ): + raise insufficient_information() + + if "journal" in dimensions and ( + entity_type != "collection" or platform.collection_type != "journals" + ): + raise insufficient_information() + + segmented = validate_segmented_period( + begin, + end, + dimensions, + metric_type, + ) + + return dimensions, entity_type, entity_id, metric_type, segmented + + def _entity_filter(self, platform, entity_type, entity_id): + if entity_type == "collection": + if entity_id != platform.acron3: + raise insufficient_information() + return None, None + + definition = DISTRIBUTION_ENTITIES[entity_type] + if platform.collection_type != definition["collection_type"]: + raise insufficient_information() + + key = self.report_query.resolve_item_id( + platform, + definition["report_id"], + entity_id, + ) + + return definition["key_field"], key + + def _query_body( + self, + platform, + begin, + end, + dimensions, + entity_type, + metric_type, + key_field, + entity_key, + segmented, + ): + data_type = DISTRIBUTION_ENTITIES[entity_type].get("data_type") + index_name, filters = usage_scope( + platform, + begin, + end, + segmented, + data_type, + ) + + if key_field: + filters.append({"term": {key_field: entity_key}}) + + sources = [] + for dimension in dimensions: + source_name = dimension + field = DISTRIBUTION_DIMENSIONS[dimension] + if segmented and dimension == "yop": + source_name = "document_key" + field = "document_key" + + sources.append( + { + source_name: { + "terms": { + "field": field, + "missing_bucket": True, + } + } + } + ) + body = { + "size": 0, + "track_total_hits": False, + "query": {"bool": {"filter": filters}}, + "aggs": { + "rows": { + "composite": {"size": PAGE_SIZE, "sources": sources}, + "aggs": {"count": {"sum": {"field": METRICS[metric_type]}}}, + } + }, + } + + return index_name, body + + def _buckets(self, index_name, body, dimensions, deadline): + totals = {} + raw_total = 0 + + for buckets in composite_pages(self.client, index_name, body, deadline): + metadata = self._metadata(buckets, dimensions, deadline) + + for bucket in buckets: + count = int(bucket["count"]["value"]) + if count <= 0: + continue + + raw_total += count + for combination in self._combinations(bucket, dimensions, metadata): + totals[combination] = totals.get(combination, 0) + count + + if len(totals) > MAX_BUCKETS: + raise insufficient_information() + + buckets = [ + { + "value": value[0] if len(value) == 1 else " | ".join(value), + "dimensions": dict(zip(dimensions, value)), + "count": count, + } + for value, count in sorted( + totals.items(), key=lambda item: (-item[1], item[0]) + ) + ] + + return raw_total, buckets + + def _metadata(self, buckets, dimensions, deadline): + metadata_dimensions = {"subject_area", "journal"} & set(dimensions) + if metadata_dimensions: + dimension = next(iter(metadata_dimensions)) + keys = [ + bucket["key"][dimension] + for bucket in buckets + if bucket["key"].get(dimension) + ] + + return fetch_metadata(self.client, "sources", keys, deadline) + + if "yop" not in dimensions: + return {} + + keys = [ + bucket["key"]["document_key"] + for bucket in buckets + if bucket["key"].get("document_key") + ] + + return fetch_metadata(self.client, "documents", keys, deadline) + + @staticmethod + def _combinations(bucket, dimensions, metadata): + combinations = [()] + + for dimension in dimensions: + value = bucket["key"].get(dimension) + if dimension == "subject_area": + source = metadata.get(value, {}) + values = set(source.get("subject_areas") or []) or {"Unknown"} + elif dimension == "journal": + source = metadata.get(value, {}) + values = {source.get("title") or value or "Unknown"} + elif dimension == "yop": + document_key = bucket["key"].get("document_key") + if document_key: + value = metadata.get(document_key, {}).get("publication_year") + values = {f"{value or 1:04d}"} + else: + values = {"Unknown" if not value or value == "_unknown" else value} + + combinations = [ + combination + (dimension_value,) + for combination in combinations + for dimension_value in values + ] + + return combinations diff --git a/counter_api/tests/test_extensions_distribution.py b/counter_api/tests/test_extensions_distribution.py new file mode 100644 index 00000000..660ae230 --- /dev/null +++ b/counter_api/tests/test_extensions_distribution.py @@ -0,0 +1,259 @@ +from datetime import date +from types import SimpleNamespace +from unittest import TestCase +from unittest.mock import Mock + +from counter_api.extensions.distribution import DistributionQuery + + +class DistributionQueryTests(TestCase): + def test_subject_areas_overlap_without_inflating_total(self): + client = Mock() + client.search.return_value = { + "aggregations": { + "rows": { + "buckets": [ + { + "key": {"subject_area": "source-1"}, + "count": {"value": 5}, + }, + { + "key": {"subject_area": "source-2"}, + "count": {"value": 2}, + }, + ] + } + } + } + client.mget.return_value = { + "docs": [ + { + "_id": "source-1", + "found": True, + "_source": {"subject_areas": ["Health", "Science"]}, + } + ] + } + query = DistributionQuery(Mock(client=client)) + platform = SimpleNamespace(acron3="scl", collection_type="journals") + + result = query.run( + platform, + date(2024, 1, 1), + date(2024, 1, 31), + { + "entity_type": "collection", + "entity_id": "scl", + "dimension": "subject_area", + }, + ) + + self.assertEqual(result["total"], 7) + self.assertEqual( + result["buckets"], + [ + { + "value": "Health", + "dimensions": {"subject_area": "Health"}, + "count": 5, + }, + { + "value": "Science", + "dimensions": {"subject_area": "Science"}, + "count": 5, + }, + { + "value": "Unknown", + "dimensions": {"subject_area": "Unknown"}, + "count": 2, + }, + ], + ) + + def test_yop_distribution_combines_missing_and_unknown_year(self): + client = Mock() + client.search.side_effect = [ + { + "aggregations": { + "rows": { + "buckets": [ + { + "key": {"yop": None}, + "count": {"value": 3}, + } + ], + "after_key": {"yop": None}, + } + } + }, + { + "aggregations": { + "rows": { + "buckets": [ + { + "key": {"yop": 1}, + "count": {"value": 2}, + }, + { + "key": {"yop": 2020}, + "count": {"value": 7}, + }, + ] + } + } + }, + ] + query = DistributionQuery(Mock(client=client)) + platform = SimpleNamespace(acron3="scl", collection_type="journals") + + result = query.run( + platform, + date(2024, 1, 1), + date(2024, 2, 29), + { + "entity_type": "collection", + "entity_id": "scl", + "dimension": "yop", + }, + ) + + self.assertEqual(result["total"], 12) + self.assertEqual( + result["buckets"], + [ + { + "value": "2020", + "dimensions": {"yop": "2020"}, + "count": 7, + }, + { + "value": "0001", + "dimensions": {"yop": "0001"}, + "count": 5, + }, + ], + ) + body = client.search.call_args_list[0].kwargs["body"] + self.assertIn( + {"terms": {"month": ["2024-01", "2024-02"]}}, + body["query"]["bool"]["filter"], + ) + + def test_country_language_cross_uses_annual_index(self): + client = Mock() + client.search.return_value = { + "aggregations": { + "rows": { + "buckets": [ + { + "key": {"country": "BR", "language": "pt"}, + "count": {"value": 9}, + } + ] + } + } + } + query = DistributionQuery(Mock(client=client)) + platform = SimpleNamespace(acron3="scl", collection_type="journals") + + result = query.run( + platform, + date(2024, 1, 1), + date(2024, 12, 31), + { + "entity_type": "collection", + "entity_id": "scl", + "dimension": "country,language", + }, + ) + + self.assertEqual(result["dimensions"], ["country", "language"]) + self.assertEqual( + result["buckets"], + [ + { + "value": "BR | pt", + "dimensions": {"country": "BR", "language": "pt"}, + "count": 9, + } + ], + ) + body = client.search.call_args.kwargs["body"] + sources = body["aggs"]["rows"]["composite"]["sources"] + self.assertEqual( + sources, + [ + { + "country": { + "terms": {"field": "country_code", "missing_bucket": True} + } + }, + { + "language": { + "terms": { + "field": "content_language", + "missing_bucket": True, + } + } + }, + ], + ) + + def test_country_yop_reads_year_from_document_metadata(self): + client = Mock() + client.search.return_value = { + "aggregations": { + "rows": { + "buckets": [ + { + "key": {"country": "BR", "document_key": "article-1"}, + "count": {"value": 9}, + } + ] + } + } + } + client.mget.return_value = { + "docs": [ + { + "_id": "article-1", + "found": True, + "_source": {"publication_year": 2020}, + } + ] + } + query = DistributionQuery(Mock(client=client)) + platform = SimpleNamespace(acron3="scl", collection_type="journals") + + result = query.run( + platform, + date(2024, 1, 1), + date(2024, 12, 31), + { + "entity_type": "collection", + "entity_id": "scl", + "dimension": "country,yop", + }, + ) + + self.assertEqual( + result["buckets"], + [ + { + "value": "BR | 2020", + "dimensions": {"country": "BR", "yop": "2020"}, + "count": 9, + } + ], + ) + sources = client.search.call_args.kwargs["body"]["aggs"]["rows"]["composite"][ + "sources" + ] + self.assertEqual( + sources[-1], + { + "document_key": { + "terms": {"field": "document_key", "missing_bucket": True} + } + }, + ) From 56f6a7c716f9bf686fa2789f16baf443645f9d66 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:45 -0300 Subject: [PATCH 37/47] =?UTF-8?q?Exporta=20an=C3=A1lises=20em=20CSV=20e=20?= =?UTF-8?q?XLSX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converte rankings e distribuições em tabelas para download com limites seguros de planilha. --- counter_api/extensions/exports.py | 75 ++++++++++++++++++++ counter_api/tests/test_extensions_exports.py | 28 ++++++++ 2 files changed, 103 insertions(+) create mode 100644 counter_api/extensions/exports.py create mode 100644 counter_api/tests/test_extensions_exports.py diff --git a/counter_api/extensions/exports.py b/counter_api/extensions/exports.py new file mode 100644 index 00000000..7b3a6403 --- /dev/null +++ b/counter_api/extensions/exports.py @@ -0,0 +1,75 @@ +import csv +from io import StringIO +from tempfile import SpooledTemporaryFile + +from openpyxl import Workbook +from openpyxl.cell import WriteOnlyCell +from openpyxl.styles import Font + +from counter_api.constants import EXCEL_MAX_ROWS, SPOOL_SIZE +from counter_api.exceptions import insufficient_information + + +def build_export(payload, kind, output_format): + if kind == "ranking": + headings = ["Country", "Language", "Rank", "ID", "Title", "Count"] + rows = ( + [ + group["dimensions"].get("country", ""), + group["dimensions"].get("language", ""), + rank, + item["id"], + item["title"], + item["count"], + ] + for group in payload["groups"] + for rank, item in enumerate(group["items"], start=1) + ) + else: + dimensions = payload.get("dimensions") or [payload["dimension"]] + headings = [dimension.replace("_", " ").title() for dimension in dimensions] + headings.append("Count") + rows = ( + [ + bucket.get("dimensions", {}).get(dimension, "") + for dimension in dimensions + ] + + [bucket["count"]] + for bucket in payload["buckets"] + ) + + output = SpooledTemporaryFile(max_size=SPOOL_SIZE) + if output_format == "csv": + line = StringIO(newline="") + writer = csv.writer(line, lineterminator="\n") + writer.writerow(headings) + output.write(line.getvalue().encode("utf-8")) + line.seek(0) + line.truncate(0) + + for row in rows: + writer.writerow(row) + output.write(line.getvalue().encode("utf-8")) + line.seek(0) + line.truncate(0) + else: + workbook = Workbook(write_only=True) + sheet = workbook.create_sheet(title=kind.capitalize()) + heading = [] + for label in headings: + cell = WriteOnlyCell(sheet, value=label) + cell.font = Font(bold=True) + heading.append(cell) + sheet.append(heading) + + for row_number, row in enumerate(rows, start=2): + if row_number > EXCEL_MAX_ROWS: + workbook.close() + output.close() + raise insufficient_information() + sheet.append(row) + + workbook.save(output) + + output.seek(0) + return output diff --git a/counter_api/tests/test_extensions_exports.py b/counter_api/tests/test_extensions_exports.py new file mode 100644 index 00000000..980cc52e --- /dev/null +++ b/counter_api/tests/test_extensions_exports.py @@ -0,0 +1,28 @@ +from io import BytesIO +from unittest import TestCase + +from openpyxl import load_workbook + +from counter_api.extensions.exports import build_export + + +class ExtensionExportTests(TestCase): + def test_ranking_csv_and_xlsx_keep_group_and_rank(self): + payload = { + "groups": [ + { + "dimensions": {"country": "BR", "language": "pt"}, + "items": [{"id": "article-1", "title": "Artigo", "count": 12}], + } + ] + } + + csv_file = build_export(payload, "ranking", "csv") + lines = csv_file.read().decode("utf-8").splitlines() + xlsx_file = build_export(payload, "ranking", "xlsx") + sheet = load_workbook(BytesIO(xlsx_file.read()), read_only=True).active + + self.assertEqual(lines[0], "Country,Language,Rank,ID,Title,Count") + self.assertEqual(lines[1], "BR,pt,1,article-1,Artigo,12") + self.assertEqual(sheet["D2"].value, "article-1") + self.assertEqual(sheet["F2"].value, 12) From e5f2bb665ea453809b3e0a65bb2ba195c71f2dc8 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:45 -0300 Subject: [PATCH 38/47] =?UTF-8?q?Descreve=20os=20contratos=20das=20extens?= =?UTF-8?q?=C3=B5es=20de=20uso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documenta capacidades, rankings e distribuições na mesma interface navegável dos relatórios COUNTER. --- counter_api/extensions/openapi.py | 55 +++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 counter_api/extensions/openapi.py diff --git a/counter_api/extensions/openapi.py b/counter_api/extensions/openapi.py new file mode 100644 index 00000000..bb4603aa --- /dev/null +++ b/counter_api/extensions/openapi.py @@ -0,0 +1,55 @@ +from rest_framework import serializers + + +class RankingItemSerializer(serializers.Serializer): + id = serializers.CharField() + title = serializers.CharField() + count = serializers.IntegerField() + + +class RankingGroupSerializer(serializers.Serializer): + dimensions = serializers.DictField() + items = RankingItemSerializer(many=True) + + +class RankingSerializer(serializers.Serializer): + platform = serializers.CharField() + begin_date = serializers.DateField() + end_date = serializers.DateField() + parent_type = serializers.CharField() + parent_id = serializers.CharField() + entity_type = serializers.CharField() + metric_type = serializers.CharField() + groups = RankingGroupSerializer(many=True) + + +class DistributionBucketSerializer(serializers.Serializer): + value = serializers.CharField() + dimensions = serializers.DictField() + count = serializers.IntegerField() + + +class DistributionSerializer(serializers.Serializer): + platform = serializers.CharField() + begin_date = serializers.DateField() + end_date = serializers.DateField() + entity_type = serializers.CharField() + entity_id = serializers.CharField() + dimension = serializers.CharField() + dimensions = serializers.ListField(child=serializers.CharField()) + metric_type = serializers.CharField() + total = serializers.IntegerField() + buckets = DistributionBucketSerializer(many=True) + + +class CapabilitiesSerializer(serializers.Serializer): + platform = serializers.CharField() + ranking_relations = serializers.ListField(child=serializers.DictField()) + ranking_metrics = serializers.ListField(child=serializers.CharField()) + ranking_dimensions = serializers.ListField(child=serializers.CharField()) + download_formats = serializers.ListField(child=serializers.CharField()) + distribution_dimensions = serializers.ListField(child=serializers.CharField()) + country_language_granularity = serializers.CharField() + other_granularity = serializers.CharField() + first_month_available = serializers.CharField(allow_null=True) + last_month_available = serializers.CharField(allow_null=True) From 7a8e57fbcf3ac2ceac2fb7866c85619d4c700735 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:45 -0300 Subject: [PATCH 39/47] =?UTF-8?q?Publica=20as=20extens=C3=B5es=20de=20uso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expõe capacidades, rankings, distribuições e downloads para consumo pelas telas do OCA. --- counter_api/extensions/urls.py | 9 + counter_api/extensions/views.py | 207 ++++++++++++++++++++++ counter_api/tests/test_extension_views.py | 66 +++++++ 3 files changed, 282 insertions(+) create mode 100644 counter_api/extensions/urls.py create mode 100644 counter_api/extensions/views.py create mode 100644 counter_api/tests/test_extension_views.py diff --git a/counter_api/extensions/urls.py b/counter_api/extensions/urls.py new file mode 100644 index 00000000..500e8d2a --- /dev/null +++ b/counter_api/extensions/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from counter_api.extensions.views import CapabilitiesView, DistributionView, RankingView + +urlpatterns = [ + path("capabilities", CapabilitiesView.as_view(), name="extension-capabilities"), + path("distribution", DistributionView.as_view(), name="extension-distribution"), + path("ranking", RankingView.as_view(), name="extension-ranking"), +] diff --git a/counter_api/extensions/views.py b/counter_api/extensions/views.py new file mode 100644 index 00000000..c4bd0aba --- /dev/null +++ b/counter_api/extensions/views.py @@ -0,0 +1,207 @@ +import logging + +from django.http import FileResponse +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema +from rest_framework.response import Response + +from counter_api.access import CounterAPIView, get_authentication_error +from counter_api.availability import available_months +from counter_api.constants import METRICS +from counter_api.dates import parse_period +from counter_api.exceptions import ( + CounterAPIError, + insufficient_information, + service_unavailable, +) +from counter_api.extensions.contracts import ( + DISTRIBUTION_CAPABILITIES, + DOWNLOAD_FORMATS, + RANKING_ENTITIES, +) +from counter_api.extensions.distribution import DistributionQuery +from counter_api.extensions.exports import build_export +from counter_api.extensions.openapi import ( + CapabilitiesSerializer, + DistributionSerializer, + RankingSerializer, +) +from counter_api.extensions.ranking import RankingQuery +from counter_api.openapi import ( + BEGIN_DATE_PARAMETER, + COUNTER_AUTH, + END_DATE_PARAMETER, + EXCEL_CONTENT_TYPE, + PLATFORM_PARAMETER, + ExceptionSerializer, +) +from counter_api.platforms import get_platform + + +class RankingView(CounterAPIView): + @extend_schema( + tags=["SciELO extensions"], + auth=COUNTER_AUTH, + operation_id="extension_ranking", + summary="Rank descendants of a collection, journal or book", + parameters=[ + PLATFORM_PARAMETER, + BEGIN_DATE_PARAMETER, + END_DATE_PARAMETER, + OpenApiParameter("parent_type", OpenApiTypes.STR, required=True), + OpenApiParameter("parent_id", OpenApiTypes.STR, required=True), + OpenApiParameter("entity_type", OpenApiTypes.STR, required=True), + OpenApiParameter("metric_type", OpenApiTypes.STR, required=False), + OpenApiParameter("group_by", OpenApiTypes.STR, required=False), + OpenApiParameter("country", OpenApiTypes.STR, required=False), + OpenApiParameter("language", OpenApiTypes.STR, required=False), + OpenApiParameter("yop", OpenApiTypes.STR, required=False), + OpenApiParameter("subject_area", OpenApiTypes.STR, required=False), + OpenApiParameter("limit", OpenApiTypes.INT, required=False), + OpenApiParameter("format", OpenApiTypes.STR, required=False), + ], + responses={ + 200: RankingSerializer, + (200, "text/csv"): OpenApiTypes.BINARY, + (200, EXCEL_CONTENT_TYPE): OpenApiTypes.BINARY, + 400: ExceptionSerializer, + 503: ExceptionSerializer, + }, + ) + def get(self, request): + authentication_error = get_authentication_error(request) + if authentication_error: + return authentication_error + + try: + output_format = request.query_params.get("format", "json") + if output_format not in DOWNLOAD_FORMATS: + raise insufficient_information() + + platform = get_platform(request.query_params.get("platform")) + begin, end = parse_period(request.query_params) + payload = RankingQuery().run(platform, begin, end, request.query_params) + + if output_format != "json": + content = build_export(payload, "ranking", output_format) + content_type = ( + "text/csv" if output_format == "csv" else EXCEL_CONTENT_TYPE + ) + response = FileResponse(content, content_type=content_type) + response[ + "Content-Disposition" + ] = f'attachment; filename="ranking_{platform.acron3}.{output_format}"' + return response + except CounterAPIError as error: + return Response(error.as_dict(), status=error.status_code) + except Exception: + logging.exception("COUNTER extension ranking failed") + error = service_unavailable() + return Response(error.as_dict(), status=error.status_code) + + return Response(payload) + + +class CapabilitiesView(CounterAPIView): + @extend_schema( + tags=["SciELO extensions"], + auth=COUNTER_AUTH, + operation_id="extension_capabilities", + summary="Discover SciELO extension capabilities for a platform", + parameters=[PLATFORM_PARAMETER], + responses={200: CapabilitiesSerializer, 400: ExceptionSerializer}, + ) + def get(self, request): + authentication_error = get_authentication_error(request) + if authentication_error: + return authentication_error + + try: + platform = get_platform(request.query_params.get("platform")) + months = available_months(platform) + except CounterAPIError as error: + return Response(error.as_dict(), status=error.status_code) + + relations = [ + {"entity_type": entity, "parent_type": relation["parent_type"]} + for entity, relation in RANKING_ENTITIES.items() + if relation["collection_type"] == platform.collection_type + ] + return Response( + { + "platform": platform.acron3, + "ranking_relations": relations, + "ranking_metrics": list(METRICS), + "ranking_dimensions": ["country", "language"], + "download_formats": list(DOWNLOAD_FORMATS), + "distribution_dimensions": list(DISTRIBUTION_CAPABILITIES), + "country_language_granularity": "year", + "other_granularity": "month", + "first_month_available": months[0].strftime("%Y-%m") + if months + else None, + "last_month_available": months[-1].strftime("%Y-%m") + if months + else None, + } + ) + + +class DistributionView(CounterAPIView): + @extend_schema( + tags=["SciELO extensions"], + auth=COUNTER_AUTH, + operation_id="extension_distribution", + summary="Distribute accesses by one dimension or a supported pair", + parameters=[ + PLATFORM_PARAMETER, + BEGIN_DATE_PARAMETER, + END_DATE_PARAMETER, + OpenApiParameter("entity_type", OpenApiTypes.STR, required=True), + OpenApiParameter("entity_id", OpenApiTypes.STR, required=True), + OpenApiParameter("dimension", OpenApiTypes.STR, required=True), + OpenApiParameter("metric_type", OpenApiTypes.STR, required=False), + OpenApiParameter("format", OpenApiTypes.STR, required=False), + ], + responses={ + 200: DistributionSerializer, + (200, "text/csv"): OpenApiTypes.BINARY, + (200, EXCEL_CONTENT_TYPE): OpenApiTypes.BINARY, + 400: ExceptionSerializer, + 503: ExceptionSerializer, + }, + ) + def get(self, request): + authentication_error = get_authentication_error(request) + if authentication_error: + return authentication_error + + try: + output_format = request.query_params.get("format", "json") + if output_format not in DOWNLOAD_FORMATS: + raise insufficient_information() + + platform = get_platform(request.query_params.get("platform")) + begin, end = parse_period(request.query_params) + payload = DistributionQuery().run( + platform, begin, end, request.query_params + ) + + if output_format != "json": + content = build_export(payload, "distribution", output_format) + content_type = ( + "text/csv" if output_format == "csv" else EXCEL_CONTENT_TYPE + ) + response = FileResponse(content, content_type=content_type) + response[ + "Content-Disposition" + ] = f'attachment; filename="distribution_{platform.acron3}.{output_format}"' + return response + except CounterAPIError as error: + return Response(error.as_dict(), status=error.status_code) + except Exception: + logging.exception("COUNTER extension distribution failed") + error = service_unavailable() + return Response(error.as_dict(), status=error.status_code) + + return Response(payload) diff --git a/counter_api/tests/test_extension_views.py b/counter_api/tests/test_extension_views.py new file mode 100644 index 00000000..96b1d691 --- /dev/null +++ b/counter_api/tests/test_extension_views.py @@ -0,0 +1,66 @@ +from datetime import date +from unittest.mock import patch + +from django.test import TestCase + +from collection.models import Collection +from counter_api.models import APIKey + + +class ExtensionViewTests(TestCase): + def setUp(self): + self.platform = Collection.objects.create( + acron3="scl", + main_name="SciELO Brasil", + collection_type="journals", + is_active=True, + ) + _key, self.api_key = APIKey.issue("tests") + + @patch("counter_api.extensions.views.available_months") + def test_extension_capabilities_show_journal_relations(self, available_months): + available_months.return_value = [date(2024, 1, 1), date(2024, 8, 1)] + + response = self.client.get( + "/r51/extensions/capabilities", + {"api_key": self.api_key, "platform": "scl"}, + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json()["ranking_relations"], + [ + {"entity_type": "journals", "parent_type": "collection"}, + {"entity_type": "articles", "parent_type": "journal"}, + ], + ) + self.assertNotIn("coverage", response.json()) + + @patch("counter_api.extensions.views.RankingQuery") + def test_extension_ranking_download_uses_same_result(self, query_class): + query_class.return_value.run.return_value = { + "groups": [ + { + "dimensions": {}, + "items": [{"id": "article-1", "title": "Artigo", "count": 12}], + } + ] + } + response = self.client.get( + "/r51/extensions/ranking", + { + "api_key": self.api_key, + "platform": "scl", + "begin_date": "2025-08", + "end_date": "2025-08", + "parent_type": "journal", + "parent_id": "Print_ISSN:1234-5678", + "entity_type": "articles", + "format": "csv", + }, + ) + + lines = b"".join(response.streaming_content).decode("utf-8").splitlines() + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "text/csv") + self.assertEqual(lines[1], ",,1,article-1,Artigo,12") From 4520d3fb74ed62718256943af3d11a55d32ca2e5 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:31:10 -0300 Subject: [PATCH 40/47] =?UTF-8?q?Publica=20as=20rotas=20e=20a=20documenta?= =?UTF-8?q?=C3=A7=C3=A3o=20COUNTER?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Monta um esquema restrito à interface, oferece Swagger e integra as rotas ao projeto Django. --- config/urls.py | 8 ++-- counter_api/api_urls.py | 11 +++++ counter_api/schema_urls.py | 6 +++ counter_api/tests/test_http.py | 79 ++++++++++++++++++++++++++++++++++ counter_api/urls.py | 19 ++++++++ 5 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 counter_api/api_urls.py create mode 100644 counter_api/schema_urls.py create mode 100644 counter_api/tests/test_http.py create mode 100644 counter_api/urls.py diff --git a/config/urls.py b/config/urls.py index 91bb5a4d..1f379b06 100644 --- a/config/urls.py +++ b/config/urls.py @@ -2,6 +2,7 @@ from django.conf.urls.i18n import i18n_patterns from django.conf.urls.static import static from django.contrib import admin +from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.urls import include, path, re_path from django.views import defaults as default_views from wagtail import urls as wagtail_urls @@ -10,6 +11,7 @@ from wagtailautocomplete.urls.admin import urlpatterns as autocomplete_admin_urls urlpatterns = [ + path("r51/", include("counter_api.urls")), path("admin/autocomplete/", include(autocomplete_admin_urls)), path(settings.DJANGO_ADMIN_URL, admin.site.urls), # Wagtail Admin @@ -30,8 +32,6 @@ if settings.DEBUG: # Wagtail settings: Serve static and media files from development server - from django.contrib.staticfiles.urls import staticfiles_urlpatterns - urlpatterns += staticfiles_urlpatterns() urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # This allows the error pages to be debugged during development, just visit @@ -55,6 +55,4 @@ path("500/", default_views.server_error), ] if "debug_toolbar" in settings.INSTALLED_APPS: - import debug_toolbar - - urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns + urlpatterns = [path("__debug__/", include("debug_toolbar.urls"))] + urlpatterns diff --git a/counter_api/api_urls.py b/counter_api/api_urls.py new file mode 100644 index 00000000..7e3e290c --- /dev/null +++ b/counter_api/api_urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from counter_api import views + +urlpatterns = [ + path("status", views.StatusView.as_view(), name="status"), + path("members", views.MembersView.as_view(), name="members"), + path("platforms", views.PlatformsView.as_view(), name="platforms"), + path("reports", views.ReportsView.as_view(), name="reports"), + path("reports/", views.ReportView.as_view(), name="report"), +] diff --git a/counter_api/schema_urls.py b/counter_api/schema_urls.py new file mode 100644 index 00000000..ff9c3ec8 --- /dev/null +++ b/counter_api/schema_urls.py @@ -0,0 +1,6 @@ +from django.urls import include, path + +urlpatterns = [ + path("r51/", include("counter_api.api_urls")), + path("r51/extensions/", include("counter_api.extensions.urls")), +] diff --git a/counter_api/tests/test_http.py b/counter_api/tests/test_http.py new file mode 100644 index 00000000..0b8ed840 --- /dev/null +++ b/counter_api/tests/test_http.py @@ -0,0 +1,79 @@ +from datetime import date +from unittest.mock import patch + +from django.test import TestCase + +from collection.models import Collection +from counter_api.models import APIKey + + +class CounterViewTests(TestCase): + def setUp(self): + self.platform = Collection.objects.create( + acron3="scl", + main_name="SciELO Brasil", + collection_type="journals", + is_active=True, + ) + _key, self.api_key = APIKey.issue("tests") + + def test_status_is_public_and_private_routes_require_api_key(self): + status = self.client.get("/r51/status") + reports = self.client.get("/r51/reports", {"platform": "scl"}) + + self.assertEqual(status.status_code, 200) + self.assertTrue(status.json()["Service_Active"]) + self.assertEqual(reports.status_code, 401) + self.assertEqual(reports.json()["Code"], 2020) + + def test_counter_api_ignores_global_jwt_authentication(self): + response = self.client.get( + "/r51/platforms", + {"api_key": self.api_key}, + HTTP_AUTHORIZATION="Bearer invalid", + ) + + self.assertEqual(response.status_code, 200) + + @patch("counter_api.views.available_months") + def test_report_catalog_uses_completed_months(self, available_months): + available_months.return_value = [date(2024, 1, 1), date(2024, 8, 1)] + + response = self.client.get( + "/r51/reports", + {"api_key": self.api_key, "platform": "scl"}, + ) + + self.assertEqual(response.status_code, 200) + self.assertEqual( + [report["Report_ID"] for report in response.json()], + ["PR", "TR", "TR_J3", "IR"], + ) + self.assertEqual(response.json()[0]["First_Month_Available"], "2024-01") + self.assertEqual(response.json()[0]["Last_Month_Available"], "2024-08") + + def test_swagger_and_schema_are_public_and_limited_to_counter(self): + docs = self.client.get("/r51/") + schema = self.client.get("/r51/schema/", HTTP_ACCEPT="application/json") + payload = schema.json() + + self.assertEqual(docs.status_code, 200) + self.assertContains(docs, "SwaggerUIBundle") + self.assertEqual(schema.status_code, 200) + self.assertEqual( + set(payload["paths"]), + { + "/r51/members", + "/r51/platforms", + "/r51/reports", + "/r51/reports/{report_id}", + "/r51/status", + "/r51/extensions/capabilities", + "/r51/extensions/distribution", + "/r51/extensions/ranking", + }, + ) + self.assertEqual( + payload["components"]["securitySchemes"]["CounterAPIKey"], + {"type": "apiKey", "in": "query", "name": "api_key"}, + ) diff --git a/counter_api/urls.py b/counter_api/urls.py new file mode 100644 index 00000000..6c67fac5 --- /dev/null +++ b/counter_api/urls.py @@ -0,0 +1,19 @@ +from django.urls import include, path +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView + +app_name = "counter_api" + +urlpatterns = [ + path( + "schema/", + SpectacularAPIView.as_view(urlconf="counter_api.schema_urls"), + name="schema", + ), + path( + "", + SpectacularSwaggerView.as_view(url_name="counter_api:schema"), + name="docs", + ), + path("", include("counter_api.api_urls")), + path("extensions/", include("counter_api.extensions.urls")), +] From 4c66733c30e16ae9ce842ef808ba4d54c770c473 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:49:30 -0300 Subject: [PATCH 41/47] =?UTF-8?q?Preserva=20o=20tipo=20de=20conte=C3=BAdo?= =?UTF-8?q?=20no=20relat=C3=B3rio=20de=20plataforma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- counter_api/query.py | 7 ++++++- counter_api/tests/test_query.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/counter_api/query.py b/counter_api/query.py index e741417a..466a002d 100644 --- a/counter_api/query.py +++ b/counter_api/query.py @@ -22,7 +22,12 @@ "access_method", "publication_year", ) -PLATFORM_GROUP_FIELDS = ("metric_scope", "access_type", "access_method") +PLATFORM_GROUP_FIELDS = ( + "metric_scope", + "data_type", + "access_type", + "access_method", +) TITLE_GROUP_FIELDS = ( "source_key", "metric_scope", diff --git a/counter_api/tests/test_query.py b/counter_api/tests/test_query.py index ef92764d..0dd798e0 100644 --- a/counter_api/tests/test_query.py +++ b/counter_api/tests/test_query.py @@ -43,7 +43,7 @@ def test_partial_shard_failure_does_not_return_report(self): with self.assertRaises(CounterAPIError): list(query.fetch(Mock(acron3="scl"), [date(2024, 1, 1)])) - def test_platform_query_does_not_group_by_items(self): + def test_platform_query_groups_by_data_type_not_items(self): query = ReportQuery(Mock(client=Mock())) body = query._query_body([date(2024, 1, 1)], None, "pr") @@ -52,6 +52,7 @@ def test_platform_query_does_not_group_by_items(self): self.assertNotIn("source_key", fields) self.assertNotIn("document_key", fields) + self.assertIn("data_type", fields) self.assertIn("month", fields) def test_query_applies_access_filters_before_aggregation(self): From c49a8cfbdcfae11ad1fb6beb089027c6f2ec1cc5 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 22:49:43 -0300 Subject: [PATCH 42/47] =?UTF-8?q?Elimina=20repeti=C3=A7=C3=B5es=20nas=20ex?= =?UTF-8?q?tens=C3=B5es=20de=20relat=C3=B3rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- counter_api/extensions/distribution.py | 18 +++++-------- counter_api/extensions/query.py | 15 +++++++++++ counter_api/extensions/ranking.py | 23 ++++++++-------- counter_api/extensions/views.py | 37 +++++++++++++++----------- counter_api/tabular.py | 10 +++---- 5 files changed, 58 insertions(+), 45 deletions(-) diff --git a/counter_api/extensions/distribution.py b/counter_api/extensions/distribution.py index 7af6636a..1b86597b 100644 --- a/counter_api/extensions/distribution.py +++ b/counter_api/extensions/distribution.py @@ -9,7 +9,11 @@ DISTRIBUTION_DIMENSIONS, DISTRIBUTION_ENTITIES, ) -from counter_api.extensions.query import usage_scope, validate_segmented_period +from counter_api.extensions.query import ( + metric_composite_body, + usage_scope, + validate_segmented_period, +) from counter_api.metadata import fetch_metadata from counter_api.query import ReportQuery from counter_api.search import composite_pages @@ -161,17 +165,7 @@ def _query_body( } } ) - body = { - "size": 0, - "track_total_hits": False, - "query": {"bool": {"filter": filters}}, - "aggs": { - "rows": { - "composite": {"size": PAGE_SIZE, "sources": sources}, - "aggs": {"count": {"sum": {"field": METRICS[metric_type]}}}, - } - }, - } + body = metric_composite_body(filters, sources, metric_type, PAGE_SIZE) return index_name, body diff --git a/counter_api/extensions/query.py b/counter_api/extensions/query.py index f687c027..f82a5a9a 100644 --- a/counter_api/extensions/query.py +++ b/counter_api/extensions/query.py @@ -1,5 +1,6 @@ from django.conf import settings +from counter_api.constants import METRICS from counter_api.dates import iter_months from counter_api.exceptions import insufficient_information, invalid_dates from counter_api.extensions.contracts import SEGMENT_DIMENSIONS @@ -9,6 +10,20 @@ ) +def metric_composite_body(filters, sources, metric_type, page_size): + return { + "size": 0, + "track_total_hits": False, + "query": {"bool": {"filter": filters}}, + "aggs": { + "rows": { + "composite": {"size": page_size, "sources": sources}, + "aggs": {"count": {"sum": {"field": METRICS[metric_type]}}}, + } + }, + } + + def validate_segmented_period(begin, end, dimensions, metric_type): segmented = bool(SEGMENT_DIMENSIONS & set(dimensions)) if not segmented: diff --git a/counter_api/extensions/ranking.py b/counter_api/extensions/ranking.py index 2349c13c..6d79aa10 100644 --- a/counter_api/extensions/ranking.py +++ b/counter_api/extensions/ranking.py @@ -6,7 +6,11 @@ from counter_api.constants import METADATA_BATCH_SIZE, METRICS from counter_api.exceptions import insufficient_information from counter_api.extensions.contracts import RANKING_ENTITIES, RANKING_GROUPS -from counter_api.extensions.query import usage_scope, validate_segmented_period +from counter_api.extensions.query import ( + metric_composite_body, + usage_scope, + validate_segmented_period, +) from counter_api.metadata import fetch_metadata from counter_api.query import ReportQuery from counter_api.search import composite_pages, search @@ -316,17 +320,12 @@ def _query_body( {field: {"terms": {"field": field, "missing_bucket": True}}} for field in dimensions ] - body = { - "size": 0, - "track_total_hits": False, - "query": {"bool": {"filter": filters}}, - "aggs": { - "rows": { - "composite": {"size": RANKING_PAGE_SIZE, "sources": sources}, - "aggs": {"count": {"sum": {"field": METRICS[metric_type]}}}, - } - }, - } + body = metric_composite_body( + filters, + sources, + metric_type, + RANKING_PAGE_SIZE, + ) return index_name, body diff --git a/counter_api/extensions/views.py b/counter_api/extensions/views.py index c4bd0aba..f5e0f975 100644 --- a/counter_api/extensions/views.py +++ b/counter_api/extensions/views.py @@ -38,6 +38,17 @@ from counter_api.platforms import get_platform +def _download_response(payload, kind, platform, output_format): + content = build_export(payload, kind, output_format) + content_type = "text/csv" if output_format == "csv" else EXCEL_CONTENT_TYPE + response = FileResponse(content, content_type=content_type) + response[ + "Content-Disposition" + ] = f'attachment; filename="{kind}_{platform.acron3}.{output_format}"' + + return response + + class RankingView(CounterAPIView): @extend_schema( tags=["SciELO extensions"], @@ -83,15 +94,12 @@ def get(self, request): payload = RankingQuery().run(platform, begin, end, request.query_params) if output_format != "json": - content = build_export(payload, "ranking", output_format) - content_type = ( - "text/csv" if output_format == "csv" else EXCEL_CONTENT_TYPE + return _download_response( + payload, + "ranking", + platform, + output_format, ) - response = FileResponse(content, content_type=content_type) - response[ - "Content-Disposition" - ] = f'attachment; filename="ranking_{platform.acron3}.{output_format}"' - return response except CounterAPIError as error: return Response(error.as_dict(), status=error.status_code) except Exception: @@ -188,15 +196,12 @@ def get(self, request): ) if output_format != "json": - content = build_export(payload, "distribution", output_format) - content_type = ( - "text/csv" if output_format == "csv" else EXCEL_CONTENT_TYPE + return _download_response( + payload, + "distribution", + platform, + output_format, ) - response = FileResponse(content, content_type=content_type) - response[ - "Content-Disposition" - ] = f'attachment; filename="distribution_{platform.acron3}.{output_format}"' - return response except CounterAPIError as error: return Response(error.as_dict(), status=error.status_code) except Exception: diff --git a/counter_api/tabular.py b/counter_api/tabular.py index 0ef24270..fd4cd2f4 100644 --- a/counter_api/tabular.py +++ b/counter_api/tabular.py @@ -111,7 +111,11 @@ def _report_rows(report): ), ("Report_Attributes", _pairs(header.get("Report_Attributes") or {})), ("Exceptions", _exceptions(header.get("Exceptions") or [])), - ("Reporting_Period", _reporting_period(header["Report_Filters"])), + ( + "Reporting_Period", + f"Begin_Date={header['Report_Filters']['Begin_Date']}; " + f"End_Date={header['Report_Filters']['End_Date']}", + ), ("Created", header["Created"]), ("Created_By", header["Created_By"]), ("Registry_Record", header.get("Registry_Record", "")), @@ -294,7 +298,3 @@ def _exceptions(values): exception += f" ({value['Data']})" result.append(exception) return "; ".join(result) - - -def _reporting_period(filters): - return f"Begin_Date={filters['Begin_Date']}; " f"End_Date={filters['End_Date']}" From 88e6abf54736307fe6e29d9b5487b97349953035 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Mon, 21 Sep 2026 23:01:29 -0300 Subject: [PATCH 43/47] =?UTF-8?q?Exclui=20arquivos=20de=20m=C3=ADdia=20das?= =?UTF-8?q?=20imagens=20Docker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 79d8c955..f23d8fba 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,5 +10,6 @@ .travis.yml .continue .envs/.local/.django +core/media temp venv From 33ac214f60e73b14f6705f44c6644072969e7bf5 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Tue, 22 Sep 2026 00:29:15 -0300 Subject: [PATCH 44/47] =?UTF-8?q?Distingue=20a=20granularidade=20JSON=20da?= =?UTF-8?q?=20exporta=C3=A7=C3=A3o=20tabular?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- counter_api/openapi.py | 10 ++++++ counter_api/parameters.py | 22 +++++++++++- counter_api/report_items.py | 2 +- counter_api/reports.py | 4 ++- counter_api/tabular.py | 3 ++ counter_api/tests/test_parameters.py | 14 ++++++++ counter_api/tests/test_reports.py | 52 +++++++++++++++++++++++++++- 7 files changed, 103 insertions(+), 4 deletions(-) diff --git a/counter_api/openapi.py b/counter_api/openapi.py index 0006b2a1..9473acd3 100644 --- a/counter_api/openapi.py +++ b/counter_api/openapi.py @@ -178,6 +178,15 @@ class CounterReportSerializer(serializers.Serializer): type=OpenApiTypes.BOOL, location=OpenApiParameter.QUERY, required=False, + description="For TSV and XLSX, omit monthly columns and retain the total.", +) +GRANULARITY_PARAMETER = OpenApiParameter( + name="granularity", + type=OpenApiTypes.STR, + location=OpenApiParameter.QUERY, + required=False, + enum=["Month", "Totals"], + description="For JSON, return monthly counts or reporting-period totals.", ) INCLUDE_PARENT_PARAMETER = OpenApiParameter( name="include_parent_details", @@ -207,6 +216,7 @@ class CounterReportSerializer(serializers.Serializer): YOP_PARAMETER, ITEM_ID_PARAMETER, EXCLUDE_MONTHLY_PARAMETER, + GRANULARITY_PARAMETER, INCLUDE_PARENT_PARAMETER, ] diff --git a/counter_api/parameters.py b/counter_api/parameters.py index 6a2c779f..76b43fe3 100644 --- a/counter_api/parameters.py +++ b/counter_api/parameters.py @@ -15,6 +15,7 @@ "exclude_monthly_details": "Exclude_Monthly_Details", "include_parent_details": "Include_Parent_Details", } +GRANULARITIES = {"Month", "Totals"} ATTRIBUTE_NAMES = {"Data_Type", "YOP", "Access_Type", "Access_Method"} DATA_TYPES = { "Article", @@ -45,6 +46,7 @@ def parse_parameters(query_params, report_id=None): exceptions = [] unsupported = [] standard_view = report_id in STANDARD_VIEWS + output_format = query_params.get("format") or "json" attributes_to_show = query_params.get("attributes_to_show") if attributes_to_show and standard_view: @@ -101,6 +103,21 @@ def parse_parameters(query_params, report_id=None): elif item_id: unsupported.append("item_id") + granularity = query_params.get("granularity") + if granularity is not None: + if standard_view or output_format != "json": + unsupported.append("granularity") + elif granularity not in GRANULARITIES: + exceptions.append( + { + "Code": 3062, + "Message": "Invalid ReportAttribute Value", + "Data": "granularity", + } + ) + elif granularity == "Totals": + params["Granularity"] = granularity + for external, internal in ATTRIBUTES.items(): value = query_params.get(external) if value is None: @@ -108,6 +125,9 @@ def parse_parameters(query_params, report_id=None): if standard_view: unsupported.append(external) continue + if internal == "Exclude_Monthly_Details" and output_format == "json": + unsupported.append(external) + continue if value.lower() not in {"true", "false"}: exceptions.append( { @@ -131,7 +151,6 @@ def parse_parameters(query_params, report_id=None): params[internal] = enabled - output_format = query_params.get("format") if output_format and output_format not in {"json", "xlsx", "tsv"}: exceptions.append( { @@ -148,6 +167,7 @@ def parse_parameters(query_params, report_id=None): "begin_date", "end_date", "format", + "granularity", "item_id", "attributes_to_show", *FILTERS, diff --git a/counter_api/report_items.py b/counter_api/report_items.py index 8f7283fb..aa8f9ad4 100644 --- a/counter_api/report_items.py +++ b/counter_api/report_items.py @@ -78,7 +78,7 @@ def _serialize_entries(report_id, entries, begin, params, deadline): performance = attributes.setdefault(entry["metric_type"], {}) monthly = entry["performance"] - if params.get("Exclude_Monthly_Details"): + if params.get("Granularity") == "Totals": performance[begin.strftime("%Y-%m")] = sum(monthly.values()) else: performance.update(monthly) diff --git a/counter_api/reports.py b/counter_api/reports.py index 086a9164..38c413cc 100644 --- a/counter_api/reports.py +++ b/counter_api/reports.py @@ -103,8 +103,10 @@ def _report_attributes(params): if params.get("Attributes_To_Show"): attributes["Attributes_To_Show"] = params["Attributes_To_Show"] - if params.get("Exclude_Monthly_Details"): + if params.get("Granularity") == "Totals": attributes["Granularity"] = "Totals" + if params.get("Exclude_Monthly_Details"): + attributes["Exclude_Monthly_Details"] = "True" if params.get("Include_Parent_Details"): attributes["Include_Parent_Details"] = "True" diff --git a/counter_api/tabular.py b/counter_api/tabular.py index fd4cd2f4..c4f35ef3 100644 --- a/counter_api/tabular.py +++ b/counter_api/tabular.py @@ -123,6 +123,9 @@ def _report_rows(report): ) months = _months(header["Report_Filters"]) + if (header.get("Report_Attributes") or {}).get("Exclude_Monthly_Details"): + months = [] + columns = _columns(header, months) yield [ f"{MONTH_LABELS[int(column[5:]) - 1]}-{column[:4]}" diff --git a/counter_api/tests/test_parameters.py b/counter_api/tests/test_parameters.py index 7050a403..e4e3864d 100644 --- a/counter_api/tests/test_parameters.py +++ b/counter_api/tests/test_parameters.py @@ -74,3 +74,17 @@ def test_yop_uses_counter_special_values_as_limits(self): params["parameter_exceptions"][0]["Data"], "0000|9999-0001", ) + + def test_granularity_and_tabular_month_exclusion_use_their_own_formats(self): + json_params = parse_parameters({"granularity": "Totals"}, "tr") + tabular_params = parse_parameters( + {"format": "tsv", "exclude_monthly_details": "true"}, + "tr", + ) + invalid_params = parse_parameters({"granularity": "Year"}, "tr") + + self.assertEqual(json_params["Granularity"], "Totals") + self.assertEqual(tabular_params["Exclude_Monthly_Details"], True) + self.assertNotIn("Exclude_Monthly_Details", json_params) + self.assertNotIn("Granularity", tabular_params) + self.assertEqual(invalid_params["parameter_exceptions"][0]["Code"], 3062) diff --git a/counter_api/tests/test_reports.py b/counter_api/tests/test_reports.py index 81aaed4e..f189e789 100644 --- a/counter_api/tests/test_reports.py +++ b/counter_api/tests/test_reports.py @@ -5,11 +5,61 @@ from openpyxl import load_workbook from counter_api.reports import build_report -from counter_api.tabular import build_workbook +from counter_api.tabular import build_tsv, build_workbook from counter_api.tests.schema import validate_report class ReportSemanticsTests(TestCase): + def test_json_totals_and_tabular_month_exclusion_preserve_usage(self): + platform = Mock(acron3="scl", collection_type="journals") + records = [ + ( + { + "key": { + "month": month, + "metric_scope": "item", + "data_type": "Article", + "access_type": "Open", + "access_method": "Regular", + }, + "total_requests": {"value": count}, + "total_investigations": {"value": count}, + "unique_requests": {"value": count}, + "unique_investigations": {"value": count}, + }, + {}, + {}, + ) + for month, count in (("2024-01", 2), ("2024-02", 3)) + ] + begin = date(2024, 1, 1) + end = date(2024, 2, 29) + + json_report = build_report( + "pr", platform, begin, end, records, {"Granularity": "Totals"} + ) + tabular_report = build_report( + "pr", platform, begin, end, records, {"Exclude_Monthly_Details": True} + ) + performance = json_report["Report_Items"][0]["Attribute_Performance"][0][ + "Performance" + ] + output = build_tsv(tabular_report) + rows = output.read().decode("utf-8-sig").splitlines() + output.close() + + self.assertEqual(performance["Total_Item_Requests"], {"2024-01": 5}) + self.assertEqual( + json_report["Report_Header"]["Report_Attributes"], {"Granularity": "Totals"} + ) + self.assertIn("Exclude_Monthly_Details=True", rows[7]) + self.assertEqual( + rows[14].split("\t"), + ["Platform", "Data_Type", "Metric_Type", "Reporting_Period_Total"], + ) + self.assertEqual(rows[15].split("\t")[-1], "5") + validate_report(json_report, "pr") + def test_empty_item_report_has_no_empty_group(self): platform = Mock( acron3="books", From cace9415888765d448dafd90d0ad0f8e5ef76978 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Tue, 22 Sep 2026 00:29:23 -0300 Subject: [PATCH 45/47] =?UTF-8?q?Exige=20cobertura=20mensal=20completa=20n?= =?UTF-8?q?as=20an=C3=A1lises=20estendidas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- counter_api/extensions/distribution.py | 3 ++ counter_api/extensions/query.py | 15 ++++++++- counter_api/extensions/ranking.py | 3 ++ .../tests/test_extensions_distribution.py | 27 +++++++++++++++- counter_api/tests/test_extensions_ranking.py | 31 ++++++++++++++++++- 5 files changed, 76 insertions(+), 3 deletions(-) diff --git a/counter_api/extensions/distribution.py b/counter_api/extensions/distribution.py index 1b86597b..5fa9e09c 100644 --- a/counter_api/extensions/distribution.py +++ b/counter_api/extensions/distribution.py @@ -11,6 +11,7 @@ ) from counter_api.extensions.query import ( metric_composite_body, + require_complete_period, usage_scope, validate_segmented_period, ) @@ -36,6 +37,8 @@ def run(self, platform, begin, end, params): end, params, ) + require_complete_period(platform, begin, end) + key_field, entity_key = self._entity_filter( platform, entity_type, diff --git a/counter_api/extensions/query.py b/counter_api/extensions/query.py index f82a5a9a..5c621463 100644 --- a/counter_api/extensions/query.py +++ b/counter_api/extensions/query.py @@ -1,8 +1,13 @@ from django.conf import settings +from counter_api.availability import split_period from counter_api.constants import METRICS from counter_api.dates import iter_months -from counter_api.exceptions import insufficient_information, invalid_dates +from counter_api.exceptions import ( + insufficient_information, + invalid_dates, + service_unavailable, +) from counter_api.extensions.contracts import SEGMENT_DIMENSIONS from metrics.opensearch.names import ( generate_analytics_index_name, @@ -24,6 +29,14 @@ def metric_composite_body(filters, sources, metric_type, page_size): } +def require_complete_period(platform, begin, end): + _, pending, unavailable = split_period(platform, begin, end) + missing = [*unavailable, *pending] + if missing: + months = "|".join(month.strftime("%Y-%m") for month in sorted(missing)) + raise service_unavailable(f"Usage data incomplete for {months}") + + def validate_segmented_period(begin, end, dimensions, metric_type): segmented = bool(SEGMENT_DIMENSIONS & set(dimensions)) if not segmented: diff --git a/counter_api/extensions/ranking.py b/counter_api/extensions/ranking.py index 6d79aa10..9baa043c 100644 --- a/counter_api/extensions/ranking.py +++ b/counter_api/extensions/ranking.py @@ -8,6 +8,7 @@ from counter_api.extensions.contracts import RANKING_ENTITIES, RANKING_GROUPS from counter_api.extensions.query import ( metric_composite_body, + require_complete_period, usage_scope, validate_segmented_period, ) @@ -42,6 +43,8 @@ def run(self, platform, begin, end, params): metric_type, segmented, ) = self._parameters(platform, begin, end, params) + require_complete_period(platform, begin, end) + parent_key = self._parent_key(platform, parent_type, parent_id) key_batches = self._key_batches(platform, params, deadline) groups = self._ranked_groups( diff --git a/counter_api/tests/test_extensions_distribution.py b/counter_api/tests/test_extensions_distribution.py index 660ae230..131997fc 100644 --- a/counter_api/tests/test_extensions_distribution.py +++ b/counter_api/tests/test_extensions_distribution.py @@ -1,12 +1,37 @@ from datetime import date from types import SimpleNamespace from unittest import TestCase -from unittest.mock import Mock +from unittest.mock import Mock, patch +from counter_api.exceptions import CounterAPIError from counter_api.extensions.distribution import DistributionQuery class DistributionQueryTests(TestCase): + def setUp(self): + period = patch( + "counter_api.extensions.query.split_period", return_value=([], [], []) + ) + self.period = period.start() + self.addCleanup(period.stop) + + def test_incomplete_month_does_not_query_metrics(self): + client = Mock() + query = DistributionQuery(Mock(client=client)) + platform = SimpleNamespace(acron3="scl", collection_type="journals") + self.period.return_value = ([], [date(2025, 2, 1)], []) + + with self.assertRaises(CounterAPIError) as error: + query.run( + platform, + date(2025, 2, 1), + date(2025, 2, 28), + {"entity_type": "collection", "entity_id": "scl", "dimension": "yop"}, + ) + + self.assertEqual(error.exception.status_code, 503) + client.search.assert_not_called() + def test_subject_areas_overlap_without_inflating_total(self): client = Mock() client.search.return_value = { diff --git a/counter_api/tests/test_extensions_ranking.py b/counter_api/tests/test_extensions_ranking.py index 00bfee9f..4a4144cb 100644 --- a/counter_api/tests/test_extensions_ranking.py +++ b/counter_api/tests/test_extensions_ranking.py @@ -1,13 +1,42 @@ from datetime import date from types import SimpleNamespace from unittest import TestCase -from unittest.mock import Mock +from unittest.mock import Mock, patch from counter_api.exceptions import CounterAPIError from counter_api.extensions.ranking import RankingQuery class RankingQueryTests(TestCase): + def setUp(self): + period = patch( + "counter_api.extensions.query.split_period", return_value=([], [], []) + ) + self.period = period.start() + self.addCleanup(period.stop) + + def test_incomplete_annual_period_does_not_query_metrics(self): + client = Mock() + query = RankingQuery(Mock(client=client)) + platform = SimpleNamespace(acron3="scl", collection_type="journals") + self.period.return_value = ([], [date(2025, 2, 1)], []) + + with self.assertRaises(CounterAPIError) as error: + query.run( + platform, + date(2025, 1, 1), + date(2025, 12, 31), + { + "entity_type": "journals", + "parent_type": "collection", + "parent_id": "scl", + "group_by": "country", + }, + ) + + self.assertEqual(error.exception.status_code, 503) + client.search.assert_not_called() + def test_ranking_reads_all_pages_and_breaks_ties_by_item_key(self): client = Mock() client.search.side_effect = [ From fb1d9a8b439d048a47328798d093f47ed2f3151d Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Tue, 22 Sep 2026 00:29:31 -0300 Subject: [PATCH 46/47] =?UTF-8?q?Impede=20duplicidade=20de=20dias=20vazios?= =?UTF-8?q?=20na=20migra=C3=A7=C3=A3o=20hist=C3=B3rica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- metrics/legacy_matomo/importer.py | 5 ++++- metrics/legacy_matomo/validation.py | 2 ++ metrics/tests/services/test_legacy_matomo_importer.py | 2 +- metrics/tests/services/test_legacy_matomo_validation.py | 6 ++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/metrics/legacy_matomo/importer.py b/metrics/legacy_matomo/importer.py index 0afa7505..290adaf8 100644 --- a/metrics/legacy_matomo/importer.py +++ b/metrics/legacy_matomo/importer.py @@ -294,7 +294,10 @@ def import_manifest( month_date = date.fromisoformat(f"{manifest['month']}-01") days = manifest["source_days"] + manifest.get("empty_days", []) - day_mask = sum(1 << (date.fromisoformat(day).day - 1) for day in days) + day_mask = 0 + for day in days: + day_mask |= 1 << (date.fromisoformat(day).day - 1) + mark_days_exported( search_client, collection.acron3, diff --git a/metrics/legacy_matomo/validation.py b/metrics/legacy_matomo/validation.py index 515ea2ad..b7e31a63 100644 --- a/metrics/legacy_matomo/validation.py +++ b/metrics/legacy_matomo/validation.py @@ -253,6 +253,8 @@ def validate_manifest( raise ValueError("Source day outside migration month: %s." % value) empty_days = manifest.get("empty_days", []) + if len(empty_days) != len(set(empty_days)): + raise ValueError("Migration empty days must be unique.") if set(source_days) & set(empty_days): raise ValueError("Source days and empty days overlap.") for value in empty_days: diff --git a/metrics/tests/services/test_legacy_matomo_importer.py b/metrics/tests/services/test_legacy_matomo_importer.py index 7de72c4f..758e1447 100644 --- a/metrics/tests/services/test_legacy_matomo_importer.py +++ b/metrics/tests/services/test_legacy_matomo_importer.py @@ -182,7 +182,7 @@ def test_import_marks_source_and_empty_days_after_refresh( mark_days, ): self.manifest["source_days"] = ["2025-08-01", "2025-08-31"] - self.manifest["empty_days"] = ["2025-08-02"] + self.manifest["empty_days"] = ["2025-08-02", "2025-08-02"] collection = SimpleNamespace( acron3="scl", log_manager_config=SimpleNamespace( diff --git a/metrics/tests/services/test_legacy_matomo_validation.py b/metrics/tests/services/test_legacy_matomo_validation.py index 1fdd81aa..5911c1b8 100644 --- a/metrics/tests/services/test_legacy_matomo_validation.py +++ b/metrics/tests/services/test_legacy_matomo_validation.py @@ -66,6 +66,12 @@ def test_rejects_manifest_without_source_days(self): with self.assertRaisesMessage(ValueError, "no source days"): validation.validate_manifest(self.manifest) + def test_rejects_duplicate_empty_days(self): + self.manifest["empty_days"] = ["2025-08-02", "2025-08-02"] + + with self.assertRaisesMessage(ValueError, "empty days must be unique"): + validation.validate_manifest(self.manifest) + def test_rejects_duplicate_payload_ids(self): path = Path(self.manifest["counter"]["path"]) with gzip.open(path, "rb") as source: From 36a05e9d46704d07fb5cad011de76066fef3db06 Mon Sep 17 00:00:00 2001 From: Rafael JPD Date: Tue, 22 Sep 2026 00:35:04 -0300 Subject: [PATCH 47/47] =?UTF-8?q?Atualiza=20a=20vers=C3=A3o=20da=20aplica?= =?UTF-8?q?=C3=A7=C3=A3o=20para=202.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 73462a5a..e70b4523 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.5.1 +2.6.0