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"
__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"

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
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.exception import InternetIsOffline
@ -30,7 +30,7 @@ class AccountClient(object):
SUMMARY_CACHE_TTL = 60 * 60 * 24 * 7
def __init__(
self, api_base_url=__pioaccount_api__, retries=3,
self, api_base_url=__accounts_api__, retries=3,
):
if api_base_url.endswith("/"):
api_base_url = api_base_url[:-1]
@ -184,7 +184,7 @@ class AccountClient(object):
)
return response
def get_account_info(self, offline):
def get_account_info(self, offline=False):
account = app.get_state_item("account")
if not account:
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
def items(cls):
return get_object_members(ManifestFileType)
return get_object_members(cls)
@classmethod
def from_uri(cls, uri):

View File

@ -19,12 +19,49 @@ import tarfile
import tempfile
from platformio import fs
from platformio.compat import get_object_members
from platformio.package.exception import PackageException
from platformio.package.manifest.parser import ManifestFileType, ManifestParserFactory
from platformio.package.manifest.schema import ManifestSchema
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):
EXCLUDE_DEFAULT = [
"._*",