Initial support for package publishing in to the registry

This commit is contained in:
Ivan Kravets
2020-05-26 22:01:32 +03:00
parent 49cc5d606b
commit 19cdc7d34a
8 changed files with 219 additions and 5 deletions

View File

@ -34,5 +34,7 @@ __license__ = "Apache Software License"
__copyright__ = "Copyright 2014-present PlatformIO" __copyright__ = "Copyright 2014-present PlatformIO"
__apiurl__ = "https://api.platformio.org" __apiurl__ = "https://api.platformio.org"
__pioaccount_api__ = "https://api.accounts.platformio.org"
__accounts_api__ = "https://api.accounts.platformio.org"
__registry_api__ = "https://api.registry.platformio.org"
__pioremote_endpoint__ = "ssl:host=remote.platformio.org:port=4413" __pioremote_endpoint__ = "ssl:host=remote.platformio.org:port=4413"

View File

@ -0,0 +1,13 @@
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@ -0,0 +1,41 @@
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from platformio import __registry_api__
from platformio.clients.rest import RESTClient
from platformio.commands.account.client import AccountClient
from platformio.package.pack import PackageType
class RegistryClient(RESTClient):
def __init__(self):
super(RegistryClient, self).__init__(base_url=__registry_api__)
def publish_package(
self, archive_path, owner=None, released_at=None, private=False
):
client = AccountClient()
if not owner:
owner = client.get_account_info(offline=True).get("profile").get("username")
with open(archive_path, "rb") as fp:
response = self.send_request(
"post",
"/v3/package/%s/%s" % (owner, PackageType.from_archive(archive_path)),
params={"private": 1 if private else 0, "released_at": released_at},
headers={
"Authorization": "Bearer %s" % client.fetch_authentication_token()
},
data=fp,
)
return response

View File

@ -0,0 +1,62 @@
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import requests.adapters
from requests.packages.urllib3.util.retry import Retry # pylint:disable=import-error
from platformio import app, util
from platformio.exception import PlatformioException
class RESTClientError(PlatformioException):
pass
class RESTClient(object):
def __init__(self, base_url):
if base_url.endswith("/"):
base_url = base_url[:-1]
self.base_url = base_url
self._session = requests.Session()
self._session.headers.update({"User-Agent": app.get_user_agent()})
retry = Retry(
total=5,
backoff_factor=1,
method_whitelist=list(Retry.DEFAULT_METHOD_WHITELIST) + ["POST"],
status_forcelist=[500, 502, 503, 504],
)
adapter = requests.adapters.HTTPAdapter(max_retries=retry)
self._session.mount(base_url, adapter)
def send_request(self, method, path, **kwargs):
# check internet before and resolve issue with 60 seconds timeout
util.internet_on(raise_exception=True)
try:
response = getattr(self._session, method)(self.base_url + path, **kwargs)
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
raise RESTClientError(e)
return self.raise_error_from_response(response)
@staticmethod
def raise_error_from_response(response, expected_codes=(200, 201, 202)):
if response.status_code in expected_codes:
try:
return response.json()
except ValueError:
pass
try:
message = response.json()["message"]
except (KeyError, ValueError):
message = response.text
raise RESTClientError(message)

View File

@ -20,7 +20,7 @@ import time
import requests.adapters import requests.adapters
from requests.packages.urllib3.util.retry import Retry # pylint:disable=import-error from requests.packages.urllib3.util.retry import Retry # pylint:disable=import-error
from platformio import __pioaccount_api__, app from platformio import __accounts_api__, app
from platformio.commands.account import exception from platformio.commands.account import exception
from platformio.exception import InternetIsOffline from platformio.exception import InternetIsOffline
@ -30,7 +30,7 @@ class AccountClient(object):
SUMMARY_CACHE_TTL = 60 * 60 * 24 * 7 SUMMARY_CACHE_TTL = 60 * 60 * 24 * 7
def __init__( def __init__(
self, api_base_url=__pioaccount_api__, retries=3, self, api_base_url=__accounts_api__, retries=3,
): ):
if api_base_url.endswith("/"): if api_base_url.endswith("/"):
api_base_url = api_base_url[:-1] api_base_url = api_base_url[:-1]
@ -184,7 +184,7 @@ class AccountClient(object):
) )
return response return response
def get_account_info(self, offline): def get_account_info(self, offline=False):
account = app.get_state_item("account") account = app.get_state_item("account")
if not account: if not account:
raise exception.AccountNotAuthorized() raise exception.AccountNotAuthorized()

View File

@ -0,0 +1,59 @@
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from datetime import datetime
import click
from platformio.clients.registry import RegistryClient
from platformio.package.pack import PackagePacker
def validate_datetime(ctx, param, value): # pylint: disable=unused-argument
try:
datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
except ValueError as e:
raise click.BadParameter(e)
return value
@click.group("package", short_help="Package Manager")
def cli():
pass
@cli.command(
"publish", short_help="Publish a package to the PlatformIO Universal Registry"
)
@click.argument("package", required=True, metavar="[source directory, tar.gz or zip]")
@click.option(
"--owner",
help="PIO Account username (could be organization username). "
"Default is set to a username of the authorized PIO Account",
)
@click.option(
"--released-at",
callback=validate_datetime,
help="Custom release date and time in the next format (UTC): 2014-06-13 17:08:52",
)
@click.option("--private", is_flag=True, help="Restricted access (not a public)")
def package_publish(package, owner, released_at, private):
p = PackagePacker(package)
archive_path = p.pack()
response = RegistryClient().publish_package(
archive_path, owner, released_at, private
)
os.remove(archive_path)
click.secho(response.get("message"), fg="green")

View File

@ -40,7 +40,7 @@ class ManifestFileType(object):
@classmethod @classmethod
def items(cls): def items(cls):
return get_object_members(ManifestFileType) return get_object_members(cls)
@classmethod @classmethod
def from_uri(cls, uri): def from_uri(cls, uri):

View File

@ -19,12 +19,49 @@ import tarfile
import tempfile import tempfile
from platformio import fs from platformio import fs
from platformio.compat import get_object_members
from platformio.package.exception import PackageException from platformio.package.exception import PackageException
from platformio.package.manifest.parser import ManifestFileType, ManifestParserFactory from platformio.package.manifest.parser import ManifestFileType, ManifestParserFactory
from platformio.package.manifest.schema import ManifestSchema from platformio.package.manifest.schema import ManifestSchema
from platformio.unpacker import FileUnpacker from platformio.unpacker import FileUnpacker
class PackageType(object):
LIBRARY = "library"
PLATFORM = "platform"
TOOL = "tool"
@classmethod
def items(cls):
return get_object_members(cls)
@classmethod
def get_manifest_map(cls):
return {
cls.PLATFORM: (ManifestFileType.PLATFORM_JSON,),
cls.LIBRARY: (
ManifestFileType.LIBRARY_JSON,
ManifestFileType.LIBRARY_PROPERTIES,
ManifestFileType.MODULE_JSON,
),
cls.TOOL: (ManifestFileType.PACKAGE_JSON,),
}
@classmethod
def from_archive(cls, path):
assert path.endswith("tar.gz")
manifest_map = cls.get_manifest_map()
with tarfile.open(path, mode="r|gz") as tf:
for t in sorted(cls.items().values()):
try:
for manifest in manifest_map[t]:
if tf.getmember(manifest):
return t
except KeyError:
pass
return None
class PackagePacker(object): class PackagePacker(object):
EXCLUDE_DEFAULT = [ EXCLUDE_DEFAULT = [
"._*", "._*",