Skip to content
Merged
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 .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: ["ubuntu-24.04", "ubuntu-22.04", "ubuntu-20.04"]
os: ["ubuntu-24.04", "ubuntu-22.04"]
python: ["3.9", "3.10", "3.11", "3.12"]
runs-on: ${{ matrix.os }}
steps:
Expand Down
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ test_coverage:
coverage combine

reformat:
isort --line-width 120 --atomic --project eduid_scimapi --recursive $(SOURCE)
black --line-length 120 --target-version py37 --skip-string-normalization $(SOURCE)
ruff --format $(SOURCE)

typecheck:
mypy --ignore-missing-imports $(SOURCE)
32 changes: 32 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[project]
name = "pyFF"
version = "2.1.5"
readme = "README.rst"
description = "Federation Feeder"
requires-python = ">=3.9"
license = {file = "LICENSE"}

authors = [
{name = "Leif Johansson", email = "leifj@sunet.se"},
{name = "Fredrik Thulin", email = "redrik@thulin.net"},
{name = "Enrique Pérez Arnaud"},
{name = "Mikael Frykholm", email = "mifr@sunet.se"},
]
maintainers = [
{name = "Mikael Frykholm", email = "mifr@sunet.se"}
]

[tool.ruff]
# Allow lines to be as long as 120.
line-length = 120
target-version = "py39"
[tool.ruff.format]
quote-style = "preserve"

[tool.build_sphinx]
source-dir = "docs/"
build-dir = "docs/build"
all_files = "1"

[tool.upload_sphinx]
upload-dir = "docs/build/html"
9 changes: 2 additions & 7 deletions src/pyff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@
pyFF is a SAML metadata aggregator.
"""

import pkg_resources
import importlib

__author__ = 'Leif Johansson'
__copyright__ = "Copyright 2009-2018 SUNET and the IdentityPython Project"
__license__ = "BSD"
__maintainer__ = "leifj@sunet.se"
__status__ = "Production"
__version__ = pkg_resources.require("pyFF")[0].version
__version__ = importlib.metadata.version('pyFF')
12 changes: 6 additions & 6 deletions src/pyff/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from json import dumps
from typing import Any, Dict, Generator, Iterable, List, Mapping, Optional, Tuple

import pkg_resources
import pyramid.httpexceptions as exc
import pytz
import requests
Expand All @@ -26,12 +25,13 @@
from pyff.resource import Resource
from pyff.samlmd import entity_display_name
from pyff.utils import b2u, dumptree, hash_id, json_serializer, utc_now
from pyff import __version__

log = get_log(__name__)


class NoCache(object):
""" Dummy implementation for when caching isn't enabled """
"""Dummy implementation for when caching isn't enabled"""

def __init__(self) -> None:
pass
Expand Down Expand Up @@ -70,7 +70,7 @@ def status_handler(request: Request) -> Response:
if 'Validation Errors' in r.info and r.info['Validation Errors']:
d[r.url] = r.info['Validation Errors']
_status = dict(
version=pkg_resources.require("pyFF")[0].version,
version=__version__,
invalids=d,
icon_store=dict(size=request.registry.md.icon_store.size()),
jobs=[dict(id=j.id, next_run_time=j.next_run_time) for j in request.registry.scheduler.get_jobs()],
Expand Down Expand Up @@ -163,7 +163,7 @@ def process_handler(request: Request) -> Response:
_ctypes = {'xml': 'application/samlmetadata+xml;application/xml;text/xml', 'json': 'application/json'}

def _d(x: Optional[str], do_split: bool = True) -> Tuple[Optional[str], Optional[str]]:
""" Split a path into a base component and an extension. """
"""Split a path into a base component and an extension."""
if x is not None:
x = x.strip()

Expand Down Expand Up @@ -214,7 +214,7 @@ def _d(x: Optional[str], do_split: bool = True) -> Tuple[Optional[str], Optional
pfx = request.registry.aliases.get(alias, None)
if pfx is None:
log.debug("alias {} not found - passing to storage lookup".format(alias))
path=alias #treat as path
path = alias # treat as path

# content_negotiation_policy is one of three values:
# 1. extension - current default, inspect the path and if it ends in
Expand Down Expand Up @@ -478,7 +478,7 @@ def cors_headers(request: Request, response: Response) -> None:
{
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST,GET,DELETE,PUT,OPTIONS',
'Access-Control-Allow-Headers': ('Origin, Content-Type, Accept, ' 'Authorization'),
'Access-Control-Allow-Headers': ('Origin, Content-Type, Accept, Authorization'),
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '1728000',
}
Expand Down
13 changes: 6 additions & 7 deletions src/pyff/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def fork(req: Plumbing.Request, *opts):
**parsecopy**

Due to a hard to find bug, fork which uses deepcopy can lose some namespaces. The parsecopy argument is a workaround.
It uses a brute force serialisation and deserialisation to get around the bug.
It uses a brute force serialisation and deserialisation to get around the bug.

.. code-block:: yaml

Expand Down Expand Up @@ -676,7 +676,7 @@ def load(req: Plumbing.Request, *opts):
url = r.pop(0)

# Copy parent node opts as a starting point
child_opts = req.md.rm.opts.copy(update={"via": [], "cleanup": [], "verify": None, "alias": url})
child_opts = req.md.rm.opts.model_copy(update={"via": [], "cleanup": [], "verify": None, "alias": url})

while len(r) > 0:
elt = r.pop(0)
Expand All @@ -702,7 +702,7 @@ def load(req: Plumbing.Request, *opts):
child_opts.verify = elt

# override anything in child_opts with what is in opts
child_opts = child_opts.copy(update=_opts)
child_opts = child_opts.model_copy(update=_opts)

req.md.rm.add_child(url, child_opts)

Expand Down Expand Up @@ -814,7 +814,7 @@ def select(req: Plumbing.Request, *opts):
else:
_opts['as'] = opts[i]
if i + 1 < len(opts):
more_opts = opts[i + 1:]
more_opts = opts[i + 1 :]
_opts.update(dict(list(zip(more_opts[::2], more_opts[1::2]))))
break

Expand All @@ -835,7 +835,6 @@ def select(req: Plumbing.Request, *opts):
entities = resolve_entities(args, lookup_fn=req.md.store.select, dedup=dedup)

if req.state.get('match', None): # TODO - allow this to be passed in via normal arguments

match = req.state['match']

if isinstance(match, six.string_types):
Expand Down Expand Up @@ -1304,14 +1303,15 @@ def xslt(req: Plumbing.Request, *opts):
if stylesheet is None:
raise PipeException("xslt requires stylesheet")

params = dict((k, "\'%s\'" % v) for (k, v) in list(req.args.items()))
params = dict((k, "'%s'" % v) for (k, v) in list(req.args.items()))
del params['stylesheet']
try:
return root(xslt_transform(req.t, stylesheet, params))
except Exception as ex:
log.debug(traceback.format_exc())
raise ex


@pipe
def indent(req: Plumbing.Request, *opts):
"""
Expand Down Expand Up @@ -1710,7 +1710,6 @@ def finalize(req: Plumbing.Request, *opts):
if name is None or 0 == len(name):
name = req.state.get('url', None)
if name and 'baseURL' in req.args:

try:
name_url = urlparse(name)
base_url = urlparse(req.args.get('baseURL'))
Expand Down
8 changes: 5 additions & 3 deletions src/pyff/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from pyff.constants import NS
from pyff.logs import get_log
from pyff.resource import Resource,ResourceInfo
from pyff.resource import Resource, ResourceInfo
from pyff.utils import find_matching_files, parse_xml, root, unicode_stream, utc_now

__author__ = 'leifj'
Expand All @@ -30,8 +30,10 @@ def _format_key(k: str) -> str:
res = {_format_key(k): v for k, v in self.dict().items()}
return res


ResourceInfo.model_rebuild()


class ParserException(Exception):
def __init__(self, msg, wrapped=None, data=None):
self._wraped = wrapped
Expand Down Expand Up @@ -84,7 +86,7 @@ def parse(self, resource: Resource, content: str) -> ParserInfo:
info = ParserInfo(description='Directory', expiration_time='never expires')
n = 0
for fn in find_matching_files(content, self.extensions):
child_opts = resource.opts.copy(update={'alias': None})
child_opts = resource.opts.model_copy(update={'alias': None})
resource.add_child("file://" + urlescape(fn), child_opts)
n += 1

Expand Down Expand Up @@ -122,7 +124,7 @@ def parse(self, resource: Resource, content: str) -> ParserInfo:
if len(fingerprints) > 0:
fp = fingerprints[0]
log.debug("XRD: {} verified by {}".format(link_href, fp))
child_opts = resource.opts.copy(update={'alias': None})
child_opts = resource.opts.model_copy(update={'alias': None})
resource.add_child(link_href, child_opts)
resource.last_seen = utc_now().replace(microsecond=0)
resource.expire_time = None
Expand Down
27 changes: 18 additions & 9 deletions src/pyff/samlmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ def find_merge_strategy(strategy_name):


def parse_saml_metadata(
source: BytesIO, opts: ResourceOpts, base_url=None, validation_errors: Optional[Dict[str, Any]] = None,
source: BytesIO,
opts: ResourceOpts,
base_url=None,
validation_errors: Optional[Dict[str, Any]] = None,
):
"""Parse a piece of XML and return an EntitiesDescriptor element after validation.

Expand Down Expand Up @@ -192,7 +195,10 @@ def _extra_md(_t, info, **kwargs):
location = kwargs.get('location')
sp_entity = sp_entities.find("{%s}EntityDescriptor[@entityID='%s']" % (NS['md'], entityID))
if sp_entity is not None:
md_source = sp_entity.find("{%s}SPSSODescriptor/{%s}Extensions/{%s}TrustInfo/{%s}MetadataSource[@src='%s']" % (NS['md'], NS['md'], NS['ti'], NS['ti'], location))
md_source = sp_entity.find(
"{%s}SPSSODescriptor/{%s}Extensions/{%s}TrustInfo/{%s}MetadataSource[@src='%s']"
% (NS['md'], NS['md'], NS['ti'], NS['ti'], location)
)
for e in iter_entities(_t):
md_source.append(e)
return etree.Element("{%s}EntitiesDescriptor" % NS['md'])
Expand All @@ -205,11 +211,14 @@ def _extra_md(_t, info, **kwargs):
entityID = e.get('entityID')
info.entities.append(entityID)

md_source = e.find("{%s}SPSSODescriptor/{%s}Extensions/{%s}TrustInfo/{%s}MetadataSource" % (NS['md'], NS['md'], NS['ti'], NS['ti']))
md_source = e.find(
"{%s}SPSSODescriptor/{%s}Extensions/{%s}TrustInfo/{%s}MetadataSource"
% (NS['md'], NS['md'], NS['ti'], NS['ti'])
)
if md_source is not None:
location = md_source.attrib.get('src')
if location is not None:
child_opts = resource.opts.copy(update={'alias': entityID})
child_opts = resource.opts.model_copy(update={'alias': entityID})
r = resource.add_child(location, child_opts)
kwargs = {
'entityID': entityID,
Expand Down Expand Up @@ -311,7 +320,7 @@ def parse(self, resource: Resource, content: str) -> EidasMDParserInfo:
info.scheme_territory, location, fp, args.get('country_code')
)
)
child_opts = resource.opts.copy(update={'alias': None})
child_opts = resource.opts.model_copy(update={'alias': None})
child_opts.verify = fp
r = resource.add_child(location, child_opts)

Expand Down Expand Up @@ -725,7 +734,6 @@ def entity_domains(entity):


def entity_extended_display_i18n(entity, default_lang=None):

name_dict = lang_dict(entity.iter("{%s}OrganizationName" % NS['md']), lambda e: e.text, default_lang=default_lang)
name_dict.update(
lang_dict(entity.iter("{%s}OrganizationDisplayName" % NS['md']), lambda e: e.text, default_lang=default_lang)
Expand Down Expand Up @@ -981,7 +989,9 @@ def discojson_sp(e, global_trust_info=None, global_md_sources=None):

sp['entityID'] = e.get('entityID', None)

md_sources = e.findall("{%s}SPSSODescriptor/{%s}Extensions/{%s}TrustInfo/{%s}MetadataSource" % (NS['md'], NS['md'], NS['ti'], NS['ti']))
md_sources = e.findall(
"{%s}SPSSODescriptor/{%s}Extensions/{%s}TrustInfo/{%s}MetadataSource" % (NS['md'], NS['md'], NS['ti'], NS['ti'])
)

sp['extra_md'] = {}
for md_source in md_sources:
Expand Down Expand Up @@ -1041,7 +1051,6 @@ def discojson_sp(e, global_trust_info=None, global_md_sources=None):


def discojson_sp_attr(e):

attribute = "https://refeds.org/entity-selection-profile"
b64_trustinfos = entity_attribute(e, attribute)
if b64_trustinfos is None:
Expand Down Expand Up @@ -1395,7 +1404,7 @@ def get_key(e):
except AttributeError:
pass
except IndexError:
log.warning("Sort pipe: unable to sort entity by '%s'. " "Entity '%s' has no such value" % (sxp, eid))
log.warning("Sort pipe: unable to sort entity by '%s'. Entity '%s' has no such value" % (sxp, eid))
except TypeError:
pass

Expand Down
9 changes: 5 additions & 4 deletions src/pyff/test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import tempfile
from unittest import TestCase

import pkg_resources
import importlib.resources
import six

from pyff import __version__ as pyffversion

# range of ports where available ports can be found
Expand Down Expand Up @@ -118,7 +117,6 @@ def _p(args, outf=None, ignore_exit=False):


class SignerTestCase(TestCase):

datadir = None
private_keyspec = None
public_keyspec = None
Expand All @@ -128,7 +126,10 @@ def sys_exit(self, code):

@classmethod
def setUpClass(cls):
cls.datadir = pkg_resources.resource_filename(__name__, 'data')
cls.datadir = importlib.resources.files(
__name__,
).joinpath('data')

cls.private_keyspec = tempfile.NamedTemporaryFile('w').name
cls.public_keyspec = tempfile.NamedTemporaryFile('w').name

Expand Down