Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
from questionpy.form import FormModel, text_input
from questionpy_common.api.qtype import QuestionTypeInterface
from questionpy_common.environment import PackageInitFunction
from questionpy_common.package_location import FunctionPackageLocation
from questionpy_sdk.webserver.server import WebServer
from questionpy_server.worker.runtime.package_location import FunctionPackageLocation


@pytest.fixture
Expand Down
2 changes: 1 addition & 1 deletion examples/dep-consumer/qpy_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ name:
languages: [de, en]
dependencies:
qpy:
- ../../local-dep-0.1.0.qpy
- "@local/dep"
26 changes: 24 additions & 2 deletions frontend/src/types/Manifest.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@

export type PackageType = 'LIBRARY' | 'QUESTIONTYPE' | 'QUESTION'
export type EnvironmentVariableName = string
export type DistQPyDependency = DistStaticQPyDependency | DistDynamicQPyDependency
export type Version = string | null
export type DependencyLockStrategy = 'required' | 'preferred-no-downgrade' | 'preferred-allow-downgrade'
export type Qpy = DistQPyDependency[]

/**
* Represents a package manifest.
Expand Down Expand Up @@ -64,9 +68,27 @@ export interface PackageFile {
size: number
}
export interface DistDependencies {
qpy: DistStaticQPyDependency[]
qpy: Qpy
}
export interface DistStaticQPyDependency {
dir_name: string
namespace: string
short_name: string
version: string
dependencies: DistDependencies1
hash: string
}
export interface DistDependencies1 {
qpy: Qpy
}
export interface DistDynamicQPyDependency {
namespace: string
short_name: string
version: Version
include_prereleases: boolean
locked: LockedDependencyInfo | null
}
export interface LockedDependencyInfo {
strategy: DependencyLockStrategy
locked_version: string
locked_hash: string
}
26 changes: 22 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "questionpy-sdk"
description = "Library and toolset for the development of QuestionPy packages"
license = "MIT"
urls = { homepage = "https://questionpy.org" }
version = "0.6.0"
version = "0.7.0"
authors = [
{ name = "TU Berlin innoCampus" },
{ email = "info@isis.tu-berlin.de" }
Expand All @@ -18,7 +18,7 @@ dependencies = [
"aiohttp >=3.11.18, <4.0.0",
"pydantic >=2.11.4, <3.0.0",
"PyYAML >=6.0.2, <7.0.0",
"questionpy-server @ git+https://github.com/questionpy-org/questionpy-server.git@32833396b7547d7a616cfc6249f2174153181bbd",
"questionpy-server @ git+https://github.com/questionpy-org/questionpy-server.git@8507508fc346470c8aac06e4cd58359f0b04998a",
"jinja2 >=3.1.6, <4.0.0",
"aiohttp-jinja2 >=1.6, <2.0",
"lxml[html-clean] >=5.4.0, <5.5.0",
Expand Down
66 changes: 66 additions & 0 deletions questionpy_sdk/_dependency_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import asyncio
import bisect
import logging
from collections.abc import Iterable, Sequence
from itertools import chain
from pathlib import Path

from semver import Version

from questionpy_common import PackageNamespaceAndShortName
from questionpy_common.package_location import PackageLocation, ZipPackageLocation
from questionpy_common.version_specifiers import QPyDependencyVersionSpecifier
from questionpy_server.dependencies import AvailablePackageVersion, DynamicDependencyResolver, NoPackageWithHashError
from questionpy_server.hash import calculate_hash
from questionpy_server.utils.manifest import ManifestError, read_manifest_from_zip


class SdkDynamicDependencyResolver(DynamicDependencyResolver):
def __init__(self, dirs: Sequence[Path]) -> None:
self._dirs = dirs

self._packages: dict[PackageNamespaceAndShortName, list[AvailablePackageVersion]] = {}
self._locations: dict[str, PackageLocation] = {}

qpy_file: Path
for qpy_file in chain.from_iterable(dir_.glob("*.qpy") for dir_ in self._dirs):
hash_ = calculate_hash(qpy_file)

try:
manifest = asyncio.run(read_manifest_from_zip(qpy_file))
except ManifestError:
_log.warning("Failed to read manifest of package '%s'", qpy_file, exc_info=True)
continue

apv = AvailablePackageVersion(manifest, hash_, Version.parse(manifest.version))

versions = self._packages.setdefault(apv.manifest.nssn, [])
bisect.insort(versions, apv, key=lambda p: p.manifest.version)
if apv.hash not in self._locations:
self._locations[apv.hash] = ZipPackageLocation(qpy_file.absolute(), hash_)

def get_matching_versions(
self,
nssn: PackageNamespaceAndShortName,
version_spec: QPyDependencyVersionSpecifier | None,
*,
include_prereleases: bool,
) -> Iterable[AvailablePackageVersion]:
versions_for_nssn = self._packages.get(nssn, ())

# We could speed this up with bisections, but it's probably not worth the effort.
return [
package_version
for package_version in versions_for_nssn
if (include_prereleases or package_version.version.prerelease is None)
and (version_spec is None or version_spec.allows(package_version.version))
]

async def get_package_location(self, hash_: str) -> PackageLocation:
try:
return self._locations[hash_]
except KeyError as e:
raise NoPackageWithHashError(hash_) from e


_log = logging.getLogger(__name__)
Loading