Сторінка 1 з 1

Як інтегрувати PyDataverse у GitHub Actions (з прикладом workflow.yml)

Додано: П'ят серпня 15, 2025 8:32 am
admin
dataverse-actions-kit.zip
(4.39 Кіб) Завантажено 14 разів
⚙️ Як інтегрувати PyDataverse у GitHub Actions (з прикладом workflow.yml)

Цей гайд показує, як автоматично оновлювати дані та метадані в DataverseUA з GitHub-репозиторію за допомогою PyDataverse і GitHub Actions. Підходить для регулярних оновлень (CI/CD), формування нових версій наборів і публікації.
В архіві окремі файли (dataverse-sync.yml, update_dataverse.py, metadata.json) - Розпакуйте в корені репозиторію GitHub, додайте Secrets, комітьте зміни в main
-----------------------------------------

Що потрібно підготувати
  • Обліковий запис у DataverseUA та API-токен.
  • DOI (persistentId) цільового датасету або створеної чернетки.
  • Репозиторій на GitHub з папками data/ (файли) та metadata/ (метадані).
  • Додайте у GitHub → Settings → Secrets and variables → Actions такі Secrets:
    • DATAVERSE_BASE_URL = https://opendata.nas.gov.ua
    • DATAVERSE_API_TOKEN = ваш_токен
    • DATASET_PERSISTENT_ID = напр. doi:10.5072/FK2/ABCDEFG
Рекомендована структура репозиторію:

Код: Виділити все

.
├─ data/
│  ├─ 2025-08/
│  │  └─ data.csv
│  └─ docs/report.pdf
└─ metadata/
   └─ metadata.json
-----------------------------------------

Приклад файлу workflow: .github/workflows/dataverse-sync.yml

Код: Виділити все

name: Dataverse Sync (PyDataverse)

on:
  push:
    branches: [ main ]
    paths:
      - 'data/**'
      - 'metadata/**'
  workflow_dispatch:
    inputs:
      publish:
        description: 'Publish dataset after upload? (true/false)'
        required: false
        default: 'false'
      release_type:
        description: 'Publish type (major/minor)'
        required: false
        default: 'minor'

permissions:
  contents: read

env:
  DATAVERSE_BASE_URL: ${{ secrets.DATAVERSE_BASE_URL }}
  DATAVERSE_API_TOKEN: ${{ secrets.DATAVERSE_API_TOKEN }}
  DATASET_PERSISTENT_ID: ${{ secrets.DATASET_PERSISTENT_ID }}
  DATA_DIR: data
  METADATA_FILE: metadata/metadata.json

jobs:
  sync:
    runs-on: ubuntu-latest
    concurrency:
      group: dataverse-sync-${{ github.ref }}
      cancel-in-progress: false

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Validate secrets
        run: |
          for v in DATAVERSE_BASE_URL DATAVERSE_API_TOKEN DATASET_PERSISTENT_ID; do
            if [ -z "${!v}" ]; then
              echo "::error title=Missing secret::$v is not set"
              exit 1
            fi
          done

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
          cache: 'pip'

      - name: Install deps
        run: |
          python -m pip install --upgrade pip
          pip install pydataverse requests python-dotenv

      - name: Create updater script
        run: |
          mkdir -p .github/scripts
          cat > .github/scripts/update_dataverse.py <<'PY'
          import os, sys, json, pathlib, requests
          from pyDataverse.api import Api

          BASE = os.environ["DATAVERSE_BASE_URL"].rstrip("/")
          TOKEN = os.environ["DATAVERSE_API_TOKEN"]
          PID   = os.environ["DATASET_PERSISTENT_ID"]
          DATA_DIR = os.environ.get("DATA_DIR", "data")
          META_FILE = os.environ.get("METADATA_FILE", "metadata/metadata.json")
          PUBLISH  = (sys.argv[1].lower() == "true") if len(sys.argv) > 1 else False
          REL_TYPE = sys.argv[2] if len(sys.argv) > 2 else "minor"

          headers = {"X-Dataverse-key": TOKEN}
          api = Api(BASE, TOKEN)

          def get_dataset_id_by_pid(pid: str) -> int:
            r = requests.get(f"{BASE}/api/datasets/:persistentId", params={"persistentId": pid})
            r.raise_for_status()
            return r.json()["data"]["id"]

          ds_id = get_dataset_id_by_pid(PID)

          # (1) Оновити метадані чернетки, якщо є metadata/metadata.json
          meta_path = pathlib.Path(META_FILE)
          if meta_path.is_file():
            with open(meta_path, "r", encoding="utf-8") as f:
              meta_payload = json.load(f)
            r = requests.put(f"{BASE}/api/datasets/{ds_id}/versions/:draft",
                             headers=headers, json=meta_payload)
            if r.status_code not in (200, 201):
              print("::warning::Metadata update failed:", r.status_code, r.text)

          # (2) Завантажити всі файли з папки data/ у чернетку набору
          data_path = pathlib.Path(DATA_DIR)
          uploaded = []
          if data_path.is_dir():
            for p in sorted(data_path.rglob("*")):
              if p.is_file():
                # directoryLabel збереже підпапки у Dataverse
                dir_label = ""
                try:
                  dir_label = str(p.parent.relative_to(data_path))
                except ValueError:
                  dir_label = ""
                json_data = {"directoryLabel": dir_label} if dir_label else {}

                files = {
                  "file": (p.name, open(p, "rb")),
                  "jsonData": (None, json.dumps(json_data))
                }
                r = requests.post(f"{BASE}/api/datasets/:persistentId/add",
                                  params={"persistentId": PID},
                                  headers=headers, files=files)
                if r.status_code not in (200, 201):
                  print(f"::warning::Upload failed for {p}: {r.status_code} {r.text}")
                else:
                  uploaded.append(str(p))
          else:
            print("::notice::No data directory found; skipping file upload")

          print(f"Uploaded files: {uploaded}")

          # (3) Опублікувати нову версію, якщо ввімкнено publish
          if PUBLISH:
            r = requests.post(f"{BASE}/api/datasets/:persistentId/actions/:publish",
                              params={"persistentId": PID, "type": REL_TYPE},
                              headers=headers)
            if r.status_code not in (200, 201):
              print("::error::Publish failed:", r.status_code, r.text)
              sys.exit(1)
            print(f"Published dataset as {REL_TYPE} release.")
          PY
          chmod +x .github/scripts/update_dataverse.py

      - name: Run updater
        run: |
          python .github/scripts/update_dataverse.py "${{ github.event.inputs.publish || 'false' }}" "${{ github.event.inputs.release_type || 'minor' }}"
-----------------------------------------

Мінімальний приклад metadata/metadata.json

Порада: це payload для PUT /api/datasets/{id}/versions/:draft (citation block).

Код: Виділити все

{
  "metadataBlocks": {
    "citation": {
      "fields": [
        { "typeName": "title", "typeClass": "primitive", "value": "Щотижневе оновлення даних" },
        { "typeName": "author", "typeClass": "compound", "value": [
          { "authorName": { "value": "Прізвище Ім'я" } }
        ]},
        { "typeName": "datasetContact", "typeClass": "compound", "value": [
          { "datasetContactEmail": { "value": "you@example.org" } }
        ]},
        { "typeName": "dsDescription", "typeClass": "compound", "value": [
          { "dsDescriptionValue": { "value": "Автоматичне оновлення через GitHub Actions (PyDataverse)." } }
        ]}
      ]
    }
  }
}
-----------------------------------------

Як це працює (коротко)
  • Тригери: будь-які зміни у папках data/** або metadata/** запускають workflow. Також можна запустити вручну (Run workflow) і обрати publish=true/false, major/minor.
  • Метадані: якщо існує metadata/metadata.json, чернетка набору оновлюється (PUT /versions/:draft).
  • Файли: усі файли з data/ додаються до чернетки (POST /datasets/:persistentId/add). Підпапки зберігаються через directoryLabel.
  • Публікація: якщо publish=true — викликається actions/:publish?type=major|minor.
-----------------------------------------

Поширені помилки та підказки

Код: Виділити все

401 Unauthorized  → неправильний або прострочений токен; перевірте DATAVERSE_API_TOKEN.
403 Forbidden     → бракує прав на оновлення набору; перевірте ролі користувача в DataverseUA.
404 Not Found     → неправильний DOI (DATASET_PERSISTENT_ID) або базовий URL.
413 Payload Too Large → занадто великий файл; завантажуйте частинами або збільшіть ліміти (на боці інстанції).
415 Unsupported Media Type → вкажіть правильний multipart/form-data для файлів (в прикладі вже так).
Додатково:
  • Тримайте секрети тільки у GitHub Secrets, не в коді.
  • Для великих наборів краще запускати workflow вручну (nightly) або з чергою.
  • Ведіть CHANGELOG.md у репозиторії й додавайте його як файл до Dataverse.
-----------------------------------------

Корисні посилання

• PyDataverse: https://pydataverse.readthedocs.io
• Dataverse API: https://guides.dataverse.org/en/latest/api/
• DataverseUA: https://opendata.nas.gov.ua

-----------------------------------------

Запитання? Діліться у відповідях: допоможемо адаптувати workflow під ваш кейс (наприклад, авто-версіювання, попередні перевірки якості, або інтеграцію з MEE/HPC).