Repository: frictionlessdata/frictionless-py

Issue:
Resource/Package don't expose all standard properties
# Overview

E.g. this will fail:
- `Package(name='abcedf', keywords=['meep', 'beep', 'boop'], version='1.0.0')` 
- `package.version`

# Tasks
- [x] Identify discrepancies between the specs and the framework
- [x] Add constructor arguments and instance properties
- [x] Add `field.rdf_type` (#599)




Discussion:

Patch:
diff --git a/frictionless/field.py b/frictionless/field.py
index 9e202b21..1fe08c45 100644
--- a/frictionless/field.py
+++ b/frictionless/field.py
@@ -27,6 +27,7 @@ class Field(Metadata):
         format? (str): field format e.g. `default`
         missing_values? (str[]): missing values
         constraints? (dict): constraints
+        rdf_type? (str): RDF type
         schema? (Schema): parent schema object
 
     Raises:
@@ -45,6 +46,7 @@ class Field(Metadata):
         format=None,
         missing_values=None,
         constraints=None,
+        rdf_type=None,
         schema=None,
     ):
         self.setinitial("name", name)
@@ -54,6 +56,7 @@ class Field(Metadata):
         self.setinitial("format", format)
         self.setinitial("missingValues", missing_values)
         self.setinitial("constraints", constraints)
+        self.setinitial("rdfType", rdf_type)
         self.__schema = schema
         self.__type = None
         super().__init__(descriptor)
@@ -126,6 +129,14 @@ class Field(Metadata):
         constraints = self.get("constraints", {})
         return self.metadata_attach("constraints", constraints)
 
+    @Metadata.property
+    def rdf_type(self):
+        """
+        Returns:
+            str?: RDF Type
+        """
+        return self.get("rdfType")
+
     @Metadata.property(
         write=lambda self, value: setitem(self.constraints, "required", value)
     )
diff --git a/frictionless/package.py b/frictionless/package.py
index f4b48f2a..aa509ca3 100644
--- a/frictionless/package.py
+++ b/frictionless/package.py
@@ -30,11 +30,20 @@ class Package(Metadata):
 
     Parameters:
         descriptor? (str|dict): package descriptor
+        resources? (dict|Resource[]): list of resource descriptors
         name? (str): package name (for machines)
+        id? (str): package id (for machines)
+        licenses? (dict[]): package licenses
+        profile? (str): profile name like 'data-package'
         title? (str): package title (for humans)
         description? (str): package description
-        profile? (str): profile name like 'data-package'
-        resources? (dict|Resource[]): list of resource descriptors
+        homepage? (str): package homepage
+        version? (str): package version
+        sources? (dict[]): package sources
+        contributors? (dict[]): package contributors
+        keywords? (str[]): package keywords
+        image? (str): package image
+        created? (str): package created
         hashing? (str): a hashing algorithm for resources
         basepath? (str): a basepath of the package
         onerror? (ignore|warn|raise): behaviour if there is an error
@@ -48,11 +57,24 @@ class Package(Metadata):
         self,
         descriptor=None,
         *,
+        # Required
+        resources=None,
+        # Recommended
         name=None,
+        id=None,
+        licenses=None,
+        profile=None,
+        # Optional
         title=None,
         description=None,
-        profile=None,
-        resources=None,
+        homepage=None,
+        version=None,
+        sources=None,
+        contributors=None,
+        keywords=None,
+        image=None,
+        created=None,
+        # Extra
         hashing=None,
         basepath=None,
         onerror="ignore",
@@ -64,11 +86,20 @@ class Package(Metadata):
             descriptor = helpers.unzip_descriptor(descriptor, "datapackage.json")
 
         # Set attributes
+        self.setinitial("resources", resources)
         self.setinitial("name", name)
+        self.setinitial("id", id)
+        self.setinitial("licenses", licenses)
+        self.setinitial("profile", profile)
         self.setinitial("title", title)
         self.setinitial("description", description)
-        self.setinitial("profile", profile)
-        self.setinitial("resources", resources)
+        self.setinitial("homepage", homepage)
+        self.setinitial("version", version)
+        self.setinitial("sources", sources)
+        self.setinitial("contributors", contributors)
+        self.setinitial("keywords", keywords)
+        self.setinitial("image", image)
+        self.setinitial("created", created)
         self.__hashing = hashing
         self.__basepath = basepath or helpers.detect_basepath(descriptor)
         self.__onerror = onerror
@@ -96,6 +127,30 @@ class Package(Metadata):
         """
         return self.get("name")
 
+    @Metadata.property
+    def id(self):
+        """
+        Returns:
+            str?: package id
+        """
+        return self.get("id")
+
+    @Metadata.property
+    def licenses(self):
+        """
+        Returns:
+            dict?: package licenses
+        """
+        return self.get("licenses")
+
+    @Metadata.property
+    def profile(self):
+        """
+        Returns:
+            str: package profile
+        """
+        return self.get("profile", config.DEFAULT_PACKAGE_PROFILE)
+
     @Metadata.property
     def title(self):
         """
@@ -113,12 +168,60 @@ class Package(Metadata):
         return self.get("description")
 
     @Metadata.property
-    def profile(self):
+    def homepage(self):
         """
         Returns:
-            str: package profile
+            str?: package homepage
         """
-        return self.get("profile", config.DEFAULT_PACKAGE_PROFILE)
+        return self.get("homepage")
+
+    @Metadata.property
+    def version(self):
+        """
+        Returns:
+            str?: package version
+        """
+        return self.get("version")
+
+    @Metadata.property
+    def sources(self):
+        """
+        Returns:
+            dict[]?: package sources
+        """
+        return self.get("sources")
+
+    @Metadata.property
+    def contributors(self):
+        """
+        Returns:
+            dict[]?: package contributors
+        """
+        return self.get("contributors")
+
+    @Metadata.property
+    def keywords(self):
+        """
+        Returns:
+            str[]?: package keywords
+        """
+        return self.get("keywords")
+
+    @Metadata.property
+    def image(self):
+        """
+        Returns:
+            str?: package image
+        """
+        return self.get("image")
+
+    @Metadata.property
+    def created(self):
+        """
+        Returns:
+            str?: package created
+        """
+        return self.get("created")
 
     @Metadata.property(cache=False, write=False)
     def hashing(self):
diff --git a/frictionless/resource.py b/frictionless/resource.py
index 2f70e62b..33af68a1 100644
--- a/frictionless/resource.py
+++ b/frictionless/resource.py
@@ -20,6 +20,7 @@ from . import errors
 from . import config
 
 
+# TODO: add mediatype?
 # TODO: rework path/data/location etc
 # TODO: rework path/data updates syncing
 class Resource(Metadata):
@@ -30,10 +31,12 @@ class Resource(Metadata):
     Public   | `from frictionless import Resource`
 
     Parameters:
-        descriptor? (str|dict): report descriptor
-        name? (str): package name (for machines)
-        title? (str): package title (for humans)
-        descriptor? (str): package descriptor
+        descriptor? (str|dict): resource descriptor
+        name? (str): resource name (for machines)
+        title? (str): resource title (for humans)
+        descriptor? (str): resource descriptor
+        licenses? (dict[]): resource licenses
+        sources? (dict[]): resource sources
         path? (str): file path
         data? (any[][]): array or data arrays
         scheme? (str): file scheme
@@ -65,6 +68,8 @@ class Resource(Metadata):
         name=None,
         title=None,
         description=None,
+        licenses=None,
+        sources=None,
         path=None,
         data=None,
         # File
@@ -108,6 +113,8 @@ class Resource(Metadata):
         self.setinitial("name", name)
         self.setinitial("title", title)
         self.setinitial("description", description)
+        self.setinitial("licenses", licenses)
+        self.setinitial("sources", sources)
         self.setinitial("profile", profile)
         self.setinitial("path", path)
         self.setinitial("data", data)
@@ -192,6 +199,22 @@ class Resource(Metadata):
         """
         return self.get("description")
 
+    @Metadata.property
+    def licenses(self):
+        """
+        Returns
+            dict[]: resource licenses
+        """
+        return self.get("licenses")
+
+    @Metadata.property
+    def sources(self):
+        """
+        Returns
+            dict[]: resource sources
+        """
+        return self.get("sources")
+
     @Metadata.property
     def profile(self):
         """
diff --git a/frictionless/table.py b/frictionless/table.py
index a3c7df8e..0d10e095 100644
--- a/frictionless/table.py
+++ b/frictionless/table.py
@@ -140,6 +140,7 @@ class Table:
         sync_schema=False,
         patch_schema=None,
         # Header
+        # TODO: while whole Table id going to be deprecated - decide for Resource
         # TODO: rename to header preserving deprecated headers OR just deprecate?
         headers=None,
         # Infer


Test patch:
diff --git a/tests/test_field.py b/tests/test_field.py
index e8fc669d..28e0b2e5 100644
--- a/tests/test_field.py
+++ b/tests/test_field.py
@@ -1,5 +1,5 @@
 import pytest
-from frictionless import Field
+from frictionless import Field, helpers
 
 
 # General
@@ -58,6 +58,33 @@ def test_field_read_cell_number_missingValues():
     assert field.read_cell("N/A") == (None, None)
 
 
+@pytest.mark.parametrize("create_descriptor", [(False,), (True,)])
+def test_field_standard_specs_properties(create_descriptor):
+    options = dict(
+        name="name",
+        title="title",
+        description="description",
+        type="type",
+        format="format",
+        missing_values="missing",
+        constraints=[],
+        rdf_type="rdf",
+    )
+    resource = (
+        Field(**options)
+        if not create_descriptor
+        else Field(helpers.create_descriptor(**options))
+    )
+    assert resource.name == "name"
+    assert resource.title == "title"
+    assert resource.description == "description"
+    assert resource.type == "type"
+    assert resource.format == "format"
+    assert resource.missing_values == "missing"
+    assert resource.constraints == []
+    assert resource.rdf_type == "rdf"
+
+
 # Constraints
 
 
diff --git a/tests/test_package.py b/tests/test_package.py
index da34f216..c0e234fc 100644
--- a/tests/test_package.py
+++ b/tests/test_package.py
@@ -152,6 +152,45 @@ def test_package_from_zip_no_descriptor(tmpdir):
     assert error.note.count("datapackage.json")
 
 
+@pytest.mark.parametrize("create_descriptor", [(False,), (True,)])
+def test_package_standard_specs_properties(create_descriptor):
+    options = dict(
+        resources=[],
+        name="name",
+        id="id",
+        licenses=[],
+        profile="profile",
+        title="title",
+        description="description",
+        homepage="homepage",
+        version="version",
+        sources=[],
+        contributors=[],
+        keywords=["keyword"],
+        image="image",
+        created="created",
+    )
+    package = (
+        Package(**options)
+        if not create_descriptor
+        else Package(helpers.create_descriptor(**options))
+    )
+    assert package.resources == []
+    assert package.name == "name"
+    assert package.id == "id"
+    assert package.licenses == []
+    assert package.profile == "profile"
+    assert package.title == "title"
+    assert package.description == "description"
+    assert package.homepage == "homepage"
+    assert package.version == "version"
+    assert package.sources == []
+    assert package.contributors == []
+    assert package.keywords == ["keyword"]
+    assert package.image == "image"
+    assert package.created == "created"
+
+
 # Resources
 
 
diff --git a/tests/test_resource.py b/tests/test_resource.py
index 14f8f8bc..cc88c919 100644
--- a/tests/test_resource.py
+++ b/tests/test_resource.py
@@ -271,6 +271,31 @@ def test_resource_source_no_path_and_no_data():
     assert resource.read_rows() == []
 
 
+@pytest.mark.parametrize("create_descriptor", [(False,), (True,)])
+def test_resource_standard_specs_properties(create_descriptor):
+    options = dict(
+        path="path",
+        name="name",
+        profile="profile",
+        title="title",
+        description="description",
+        licenses=[],
+        sources=[],
+    )
+    resource = (
+        Resource(**options)
+        if not create_descriptor
+        else Resource(helpers.create_descriptor(**options))
+    )
+    assert resource.path == "path"
+    assert resource.name == "name"
+    assert resource.profile == "profile"
+    assert resource.title == "title"
+    assert resource.description == "description"
+    assert resource.licenses == []
+    assert resource.sources == []
+
+
 # Dialect
 
 
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 3965425c..36e3236c 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -4,7 +4,8 @@ import yaml
 import pytest
 import requests
 from decimal import Decimal
-from frictionless import Schema, Field, FrictionlessException, describe_schema
+from frictionless import Schema, Field, describe_schema, helpers
+from frictionless import FrictionlessException
 
 
 # General
@@ -254,6 +255,25 @@ def test_schema_metadata_error_message():
     assert "is not valid under any of the given schema" in note
 
 
+@pytest.mark.parametrize("create_descriptor", [(False,), (True,)])
+def test_schema_standard_specs_properties(create_descriptor):
+    options = dict(
+        fields=[],
+        missing_values=[],
+        primary_key=[],
+        foreign_keys=[],
+    )
+    schema = (
+        Schema(**options)
+        if not create_descriptor
+        else Schema(helpers.create_descriptor(**options))
+    )
+    assert schema.fields == []
+    assert schema.missing_values == []
+    assert schema.primary_key == []
+    assert schema.foreign_keys == []
+
+
 # Expand