glpkg 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glpkg-0.0.1/LICENSE.md +7 -0
- glpkg-0.0.1/PKG-INFO +127 -0
- glpkg-0.0.1/README.md +108 -0
- glpkg-0.0.1/pyproject.toml +42 -0
- glpkg-0.0.1/setup.cfg +4 -0
- glpkg-0.0.1/src/gitlab/__init__.py +3 -0
- glpkg-0.0.1/src/gitlab/__main__.py +12 -0
- glpkg-0.0.1/src/gitlab/cli_handler.py +134 -0
- glpkg-0.0.1/src/gitlab/packages.py +130 -0
- glpkg-0.0.1/src/glpkg.egg-info/PKG-INFO +127 -0
- glpkg-0.0.1/src/glpkg.egg-info/SOURCES.txt +14 -0
- glpkg-0.0.1/src/glpkg.egg-info/dependency_links.txt +1 -0
- glpkg-0.0.1/src/glpkg.egg-info/entry_points.txt +2 -0
- glpkg-0.0.1/src/glpkg.egg-info/requires.txt +8 -0
- glpkg-0.0.1/src/glpkg.egg-info/top_level.txt +1 -0
- glpkg-0.0.1/test/test_gitlab.py +115 -0
glpkg-0.0.1/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2025 bugproduction
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
glpkg-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: glpkg
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Tool to make GitLab generic package registry operations easy.
|
|
5
|
+
Author-email: bugproduction <bugproduction@outlook.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://gitlab.com/bugproduction/glpkg.git
|
|
8
|
+
Keywords: GitLab,packages,registry,generic
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE.md
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: black; extra == "dev"
|
|
13
|
+
Requires-Dist: build; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
16
|
+
Requires-Dist: setuptools; extra == "dev"
|
|
17
|
+
Requires-Dist: twine; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# glpkg - GitLab Generic Package registry tools
|
|
21
|
+
|
|
22
|
+
glpkg is a tool that makes it easy to work with [GitLab generic package registry](https://docs.gitlab.com/user/packages/generic_packages/).
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Install the tool from with pip:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install glpkg
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
To check the installation and version, run:
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
glpkg --version
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If you see a version in the terminal, you're good to go!
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
By default, the used GitLab host is gitlab.com. If you use a self-hosted GitLab, use argument `--host my-gitlab.net` with the commands.
|
|
45
|
+
|
|
46
|
+
> Only https scheme is supported.
|
|
47
|
+
|
|
48
|
+
To authenticate with the package registry in any of the commands below, use `--token readapitoken123` argument where the `readapitoken123` is a [personal](https://docs.gitlab.com/user/profile/personal_access_tokens/#create-a-personal-access-token) or [project](https://docs.gitlab.com/user/project/settings/project_access_tokens/#create-a-project-access-token) access token, with read API scope. In case the package registry is public, you can omit this argument.
|
|
49
|
+
|
|
50
|
+
The above arguments are omitted in the examples below to focus on the functions. Add the arguments to change the host or to authenticate with the registry.
|
|
51
|
+
|
|
52
|
+
In general, run `glpkg --help` when needed.
|
|
53
|
+
|
|
54
|
+
### Listing package versions
|
|
55
|
+
|
|
56
|
+
To list the versions of a generic package, run
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
glpkg list --project 12345 --name mypackagename
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Where:
|
|
63
|
+
- `12345` is your projects ID ([Find the Project ID](https://docs.gitlab.com/user/project/working_with_projects/#find-the-project-id)) or the path of the project (like `namespace/project`)
|
|
64
|
+
- `mypackagename` is the name of the generic package
|
|
65
|
+
|
|
66
|
+
The output will be, if package is found, something like:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
Name Version
|
|
70
|
+
mypackagename 1.0
|
|
71
|
+
mypackagename 1.5
|
|
72
|
+
mypackagename 2.0
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Download generic package
|
|
76
|
+
|
|
77
|
+
To download everything from a specific generic package, run
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
glpkg download --project 12345 --name mypackagename --version 1.0
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Where:
|
|
84
|
+
- `12345` is your projects ID ([Find the Project ID](https://docs.gitlab.com/user/project/working_with_projects/#find-the-project-id)) or the path of the project (like `namespace/project`)
|
|
85
|
+
- `mypackagename` is the name of the generic package
|
|
86
|
+
- `1.0` is the version of the generic package from which the files are downloaded
|
|
87
|
+
|
|
88
|
+
The files will be downloaded in the current working directory. Any pre-existing files will be overridden without warning.
|
|
89
|
+
|
|
90
|
+
> If a package has multiple files with the same filename, the tool can only download the newest file. This is a restriction of GitLab API.
|
|
91
|
+
|
|
92
|
+
### Upload a file to a generic package
|
|
93
|
+
|
|
94
|
+
To upload a file to a version of a generic package, run
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
glpkg upload --project 12345 --name mypackagename --version 1.0 --file my-file.txt
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Where:
|
|
101
|
+
- `12345` is your projects ID ([Find the Project ID](https://docs.gitlab.com/user/project/working_with_projects/#find-the-project-id)) or the path of the project (like `namespace/project`)
|
|
102
|
+
- `mypackagename` is the name of the generic package
|
|
103
|
+
- `1.0` is the version of the generic package to which the file is uploaded
|
|
104
|
+
- `my-file.txt` is the file that is uploaded to the generic package. Currently, only relative paths are supported, and the relative path (e.g. `folder/file.txt`) is preserved when uploading the file to the registry.
|
|
105
|
+
|
|
106
|
+
> A GitLab generic package may have multiple files with the same file name. However, it likely is not a great idea, as they cannot be downloaded separately from the GitLab API.
|
|
107
|
+
|
|
108
|
+
### Use in GitLab pipelines
|
|
109
|
+
|
|
110
|
+
If you use the tool in a GitLab pipeline, using `--ci` argument uses [GitLab predefined variables](https://docs.gitlab.com/ci/variables/predefined_variables/) to configure the tool. In this case `CI_SERVER_HOST`, `CI_PROJECT_ID`, and `CI_JOB_TOKEN` environment variables are used. The `--project`, and `--token` arguments can still be used to override the project ID and to use a personal or project access token instead of CI_JOB_TOKEN.
|
|
111
|
+
|
|
112
|
+
In other words, you don't need to give the `--host`, `--project`, or `--token` arguments if you are interacting with the package registry of the project where the pipeline is running. Example: uploading `my-file.txt` to generic package `mypackagename` version `1.0` in the project package registry in CI:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
glpkg upload --ci --name mypackagename --version 1.0 --file my-file.txt
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
To use the `CI_JOB_TOKEN` with package registry of another projects, add `--project <otherproject ID>` argument. Remember that you may need to add [permissions for the CI_JOB_TOKEN](https://docs.gitlab.com/ci/jobs/ci_job_token/#control-job-token-access-to-your-project) in the other project.
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## Limitations
|
|
122
|
+
|
|
123
|
+
The tool is not perfect (yet) and has limitations. The following limitations are known, but more can exist:
|
|
124
|
+
|
|
125
|
+
- Uploading files must be done one-by-one.
|
|
126
|
+
- Only project registries are supported for now.
|
|
127
|
+
- Pagination is not supported for now - in case you have more than 100 versions of a package, not all will be shown.
|
glpkg-0.0.1/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# glpkg - GitLab Generic Package registry tools
|
|
2
|
+
|
|
3
|
+
glpkg is a tool that makes it easy to work with [GitLab generic package registry](https://docs.gitlab.com/user/packages/generic_packages/).
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
Install the tool from with pip:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install glpkg
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
To check the installation and version, run:
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
glpkg --version
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If you see a version in the terminal, you're good to go!
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
By default, the used GitLab host is gitlab.com. If you use a self-hosted GitLab, use argument `--host my-gitlab.net` with the commands.
|
|
26
|
+
|
|
27
|
+
> Only https scheme is supported.
|
|
28
|
+
|
|
29
|
+
To authenticate with the package registry in any of the commands below, use `--token readapitoken123` argument where the `readapitoken123` is a [personal](https://docs.gitlab.com/user/profile/personal_access_tokens/#create-a-personal-access-token) or [project](https://docs.gitlab.com/user/project/settings/project_access_tokens/#create-a-project-access-token) access token, with read API scope. In case the package registry is public, you can omit this argument.
|
|
30
|
+
|
|
31
|
+
The above arguments are omitted in the examples below to focus on the functions. Add the arguments to change the host or to authenticate with the registry.
|
|
32
|
+
|
|
33
|
+
In general, run `glpkg --help` when needed.
|
|
34
|
+
|
|
35
|
+
### Listing package versions
|
|
36
|
+
|
|
37
|
+
To list the versions of a generic package, run
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
glpkg list --project 12345 --name mypackagename
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Where:
|
|
44
|
+
- `12345` is your projects ID ([Find the Project ID](https://docs.gitlab.com/user/project/working_with_projects/#find-the-project-id)) or the path of the project (like `namespace/project`)
|
|
45
|
+
- `mypackagename` is the name of the generic package
|
|
46
|
+
|
|
47
|
+
The output will be, if package is found, something like:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
Name Version
|
|
51
|
+
mypackagename 1.0
|
|
52
|
+
mypackagename 1.5
|
|
53
|
+
mypackagename 2.0
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Download generic package
|
|
57
|
+
|
|
58
|
+
To download everything from a specific generic package, run
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
glpkg download --project 12345 --name mypackagename --version 1.0
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Where:
|
|
65
|
+
- `12345` is your projects ID ([Find the Project ID](https://docs.gitlab.com/user/project/working_with_projects/#find-the-project-id)) or the path of the project (like `namespace/project`)
|
|
66
|
+
- `mypackagename` is the name of the generic package
|
|
67
|
+
- `1.0` is the version of the generic package from which the files are downloaded
|
|
68
|
+
|
|
69
|
+
The files will be downloaded in the current working directory. Any pre-existing files will be overridden without warning.
|
|
70
|
+
|
|
71
|
+
> If a package has multiple files with the same filename, the tool can only download the newest file. This is a restriction of GitLab API.
|
|
72
|
+
|
|
73
|
+
### Upload a file to a generic package
|
|
74
|
+
|
|
75
|
+
To upload a file to a version of a generic package, run
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
glpkg upload --project 12345 --name mypackagename --version 1.0 --file my-file.txt
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Where:
|
|
82
|
+
- `12345` is your projects ID ([Find the Project ID](https://docs.gitlab.com/user/project/working_with_projects/#find-the-project-id)) or the path of the project (like `namespace/project`)
|
|
83
|
+
- `mypackagename` is the name of the generic package
|
|
84
|
+
- `1.0` is the version of the generic package to which the file is uploaded
|
|
85
|
+
- `my-file.txt` is the file that is uploaded to the generic package. Currently, only relative paths are supported, and the relative path (e.g. `folder/file.txt`) is preserved when uploading the file to the registry.
|
|
86
|
+
|
|
87
|
+
> A GitLab generic package may have multiple files with the same file name. However, it likely is not a great idea, as they cannot be downloaded separately from the GitLab API.
|
|
88
|
+
|
|
89
|
+
### Use in GitLab pipelines
|
|
90
|
+
|
|
91
|
+
If you use the tool in a GitLab pipeline, using `--ci` argument uses [GitLab predefined variables](https://docs.gitlab.com/ci/variables/predefined_variables/) to configure the tool. In this case `CI_SERVER_HOST`, `CI_PROJECT_ID`, and `CI_JOB_TOKEN` environment variables are used. The `--project`, and `--token` arguments can still be used to override the project ID and to use a personal or project access token instead of CI_JOB_TOKEN.
|
|
92
|
+
|
|
93
|
+
In other words, you don't need to give the `--host`, `--project`, or `--token` arguments if you are interacting with the package registry of the project where the pipeline is running. Example: uploading `my-file.txt` to generic package `mypackagename` version `1.0` in the project package registry in CI:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
glpkg upload --ci --name mypackagename --version 1.0 --file my-file.txt
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
To use the `CI_JOB_TOKEN` with package registry of another projects, add `--project <otherproject ID>` argument. Remember that you may need to add [permissions for the CI_JOB_TOKEN](https://docs.gitlab.com/ci/jobs/ci_job_token/#control-job-token-access-to-your-project) in the other project.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Limitations
|
|
103
|
+
|
|
104
|
+
The tool is not perfect (yet) and has limitations. The following limitations are known, but more can exist:
|
|
105
|
+
|
|
106
|
+
- Uploading files must be done one-by-one.
|
|
107
|
+
- Only project registries are supported for now.
|
|
108
|
+
- Pagination is not supported for now - in case you have more than 100 versions of a package, not all will be shown.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools"]
|
|
3
|
+
|
|
4
|
+
[project]
|
|
5
|
+
name = "glpkg"
|
|
6
|
+
dynamic = ["version"]
|
|
7
|
+
description = "Tool to make GitLab generic package registry operations easy."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
keywords = ["GitLab", "packages", "registry", "generic"]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE.md"]
|
|
12
|
+
authors = [
|
|
13
|
+
{ name="bugproduction", email="bugproduction@outlook.com" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = []
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Repository = "https://gitlab.com/bugproduction/glpkg.git"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = [
|
|
22
|
+
"black",
|
|
23
|
+
"build",
|
|
24
|
+
"pytest",
|
|
25
|
+
"pytest-cov",
|
|
26
|
+
"setuptools",
|
|
27
|
+
"twine"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
glpkg = "gitlab.__main__:cli"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.dynamic]
|
|
35
|
+
version = {attr = "gitlab.__version__"}
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
pythonpath = ["src"]
|
|
42
|
+
addopts = "--cov=src --cov-report term"
|
glpkg-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
from gitlab import Packages, __version__
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CLIHandler:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
parser = argparse.ArgumentParser(
|
|
9
|
+
description="Toolbox for GitLab generic packages"
|
|
10
|
+
)
|
|
11
|
+
parser.add_argument("-v", "--version", action="store_true")
|
|
12
|
+
parser.set_defaults(action=self._print_version)
|
|
13
|
+
subparsers = parser.add_subparsers()
|
|
14
|
+
list_parser = subparsers.add_parser(
|
|
15
|
+
name="list",
|
|
16
|
+
description="Lists the available version of a package from the package registry.",
|
|
17
|
+
)
|
|
18
|
+
self._register_list_parser(list_parser)
|
|
19
|
+
download_parser = subparsers.add_parser(
|
|
20
|
+
name="download",
|
|
21
|
+
description="Downloads all files from a specific package version to the current directory.",
|
|
22
|
+
)
|
|
23
|
+
self._register_download_parser(download_parser)
|
|
24
|
+
upload_parser = subparsers.add_parser(
|
|
25
|
+
name="upload", description="Uploads file to a specific package version."
|
|
26
|
+
)
|
|
27
|
+
self._register_upload_parser(upload_parser)
|
|
28
|
+
self.args = parser.parse_args()
|
|
29
|
+
|
|
30
|
+
def _print_version(self, args) -> int:
|
|
31
|
+
print(__version__)
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
def do_it(self) -> int:
|
|
35
|
+
return self.args.action(self.args)
|
|
36
|
+
|
|
37
|
+
def _register_common_arguments(self, parser) -> None:
|
|
38
|
+
group = parser.add_mutually_exclusive_group()
|
|
39
|
+
group.add_argument(
|
|
40
|
+
"-H",
|
|
41
|
+
"--host",
|
|
42
|
+
default="gitlab.com",
|
|
43
|
+
type=str,
|
|
44
|
+
help="The host address of GitLab instance without scheme, for example gitlab.com. Note that only https scheme is supported.",
|
|
45
|
+
)
|
|
46
|
+
group.add_argument(
|
|
47
|
+
"-c",
|
|
48
|
+
"--ci",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="Use this to run the tool in GitLab pipelines. In this case CI_SERVER_HOST, CI_PROJECT_ID, and CI_JOB_TOKEN variables from the environment are used. --project and --token can be used to override project ID and the CI_JOB_TOKEN to a personal or project access token.",
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"-p",
|
|
54
|
+
"--project",
|
|
55
|
+
type=str,
|
|
56
|
+
help="The project ID or path. For example 123456 or namespace/project.",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument("-n", "--name", type=str, help="The package name.")
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"-t",
|
|
61
|
+
"--token",
|
|
62
|
+
type=str,
|
|
63
|
+
help="Private or project access token that is used to authenticate with the package registry. Leave empty if the registry is public. The token must have 'read API' or 'API' scope.",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def _register_download_parser(self, parser):
|
|
67
|
+
self._register_common_arguments(parser)
|
|
68
|
+
parser.add_argument("-v", "--version", type=str, help="The package version.")
|
|
69
|
+
parser.set_defaults(action=self._download_handler)
|
|
70
|
+
|
|
71
|
+
def _args(self, args):
|
|
72
|
+
if args.ci:
|
|
73
|
+
host = os.environ["CI_SERVER_HOST"]
|
|
74
|
+
if args.project:
|
|
75
|
+
project = args.project
|
|
76
|
+
else:
|
|
77
|
+
project = os.environ["CI_PROJECT_ID"]
|
|
78
|
+
if args.token:
|
|
79
|
+
token_user = "PRIVATE-TOKEN"
|
|
80
|
+
token = args.token
|
|
81
|
+
else:
|
|
82
|
+
token_user = "JOB-TOKEN"
|
|
83
|
+
token = os.environ["CI_JOB_TOKEN"]
|
|
84
|
+
else:
|
|
85
|
+
host = args.host
|
|
86
|
+
project = args.project
|
|
87
|
+
token_user = "PRIVATE-TOKEN"
|
|
88
|
+
token = args.token
|
|
89
|
+
name = args.name
|
|
90
|
+
return host, project, name, token_user, token
|
|
91
|
+
|
|
92
|
+
def _download_handler(self, args) -> int:
|
|
93
|
+
host, project, name, token_user, token = self._args(args)
|
|
94
|
+
version = args.version
|
|
95
|
+
gitlab = Packages(host, token_user, token)
|
|
96
|
+
package_id = gitlab.get_package_id(project, name, version)
|
|
97
|
+
files = gitlab.list_files(project, package_id)
|
|
98
|
+
ret = 1
|
|
99
|
+
for file in files:
|
|
100
|
+
ret = gitlab.download_file(project, name, version, file)
|
|
101
|
+
if not ret:
|
|
102
|
+
break
|
|
103
|
+
return ret
|
|
104
|
+
|
|
105
|
+
def _register_list_parser(self, parser):
|
|
106
|
+
self._register_common_arguments(parser)
|
|
107
|
+
parser.set_defaults(action=self._list_packages)
|
|
108
|
+
|
|
109
|
+
def _list_packages(self, args: argparse.Namespace) -> int:
|
|
110
|
+
host, project, name, token_user, token = self._args(args)
|
|
111
|
+
gitlab = Packages(host, token_user, token)
|
|
112
|
+
packages = gitlab.list_packages(project, name)
|
|
113
|
+
print("Name" + "\t\t" + "Version")
|
|
114
|
+
for package in packages:
|
|
115
|
+
print(package["name"] + "\t" + package["version"])
|
|
116
|
+
|
|
117
|
+
def _register_upload_parser(self, parser):
|
|
118
|
+
self._register_common_arguments(parser)
|
|
119
|
+
parser.add_argument("-v", "--version", type=str, help="The package version.")
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"-f",
|
|
122
|
+
"--file",
|
|
123
|
+
type=str,
|
|
124
|
+
help="The file to be uploaded, for example my_file.txt. Note that only relative paths are supported and the relative path is preserved when uploading the file.",
|
|
125
|
+
)
|
|
126
|
+
parser.set_defaults(action=self._upload)
|
|
127
|
+
|
|
128
|
+
def _upload(self, args) -> int:
|
|
129
|
+
host, project, name, token_user, token = self._args(args)
|
|
130
|
+
version = args.version
|
|
131
|
+
file = args.file
|
|
132
|
+
gitlab = Packages(host, token_user, token)
|
|
133
|
+
ret = gitlab.upload_file(project, name, version, file)
|
|
134
|
+
return ret
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from urllib import request, parse
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Packages:
|
|
6
|
+
def __init__(self, host: str, token_type: str, token: str):
|
|
7
|
+
self.host = host
|
|
8
|
+
self.token_type = token_type
|
|
9
|
+
self.token = token
|
|
10
|
+
|
|
11
|
+
def api_url(self) -> str:
|
|
12
|
+
return "https://{}/api/v4/".format(parse.quote(self.host))
|
|
13
|
+
|
|
14
|
+
def project_api_url(self, project: str) -> str:
|
|
15
|
+
return self.api_url() + "projects/{}/".format(parse.quote_plus(project))
|
|
16
|
+
|
|
17
|
+
def get_headers(self):
|
|
18
|
+
headers = {}
|
|
19
|
+
if self.token_type and self.token:
|
|
20
|
+
headers = {self.token_type: self.token}
|
|
21
|
+
return headers
|
|
22
|
+
|
|
23
|
+
def list_packages(self, project: str, package_name: str) -> list:
|
|
24
|
+
packages = []
|
|
25
|
+
with request.urlopen(
|
|
26
|
+
request.Request(
|
|
27
|
+
self.project_api_url(project)
|
|
28
|
+
+ "packages?package_name="
|
|
29
|
+
+ parse.quote_plus(package_name),
|
|
30
|
+
headers=self.get_headers(),
|
|
31
|
+
)
|
|
32
|
+
) as res:
|
|
33
|
+
data = res.read()
|
|
34
|
+
for package in json.loads(data):
|
|
35
|
+
name = parse.unquote(package["name"])
|
|
36
|
+
version = parse.unquote(package["version"])
|
|
37
|
+
# The GitLab API returns packages that have some match to the filter;
|
|
38
|
+
# let's filter out non-exact matches
|
|
39
|
+
if package_name != name:
|
|
40
|
+
continue
|
|
41
|
+
packages.append({"name": name, "version": version})
|
|
42
|
+
return packages
|
|
43
|
+
|
|
44
|
+
def list_files(self, project: str, package_id: int) -> list:
|
|
45
|
+
files = []
|
|
46
|
+
with request.urlopen(
|
|
47
|
+
request.Request(
|
|
48
|
+
self.project_api_url(project)
|
|
49
|
+
+ "packages/"
|
|
50
|
+
+ parse.quote_plus(str(package_id))
|
|
51
|
+
+ "/package_files",
|
|
52
|
+
headers=self.get_headers(),
|
|
53
|
+
)
|
|
54
|
+
) as x:
|
|
55
|
+
data = x.read()
|
|
56
|
+
for package in json.loads(
|
|
57
|
+
data,
|
|
58
|
+
):
|
|
59
|
+
# Only append the filename once to the list of files
|
|
60
|
+
# as there's no way to download them separately through
|
|
61
|
+
# the API
|
|
62
|
+
filename = parse.unquote(package["file_name"])
|
|
63
|
+
if filename not in files:
|
|
64
|
+
files.append(filename)
|
|
65
|
+
return files
|
|
66
|
+
|
|
67
|
+
def get_package_id(
|
|
68
|
+
self, project: str, package_name: str, package_version: str
|
|
69
|
+
) -> int:
|
|
70
|
+
id = 0
|
|
71
|
+
with request.urlopen(
|
|
72
|
+
request.Request(
|
|
73
|
+
self.project_api_url(project)
|
|
74
|
+
+ "packages?package_name="
|
|
75
|
+
+ parse.quote_plus(package_name)
|
|
76
|
+
+ "&package_version="
|
|
77
|
+
+ parse.quote_plus(package_version),
|
|
78
|
+
headers=self.get_headers(),
|
|
79
|
+
)
|
|
80
|
+
) as res:
|
|
81
|
+
data = res.read()
|
|
82
|
+
package = json.loads(data)
|
|
83
|
+
if len(package) == 1:
|
|
84
|
+
package = package.pop()
|
|
85
|
+
id = package["id"]
|
|
86
|
+
return id
|
|
87
|
+
|
|
88
|
+
def download_file(
|
|
89
|
+
self, project: str, package_name: str, package_version: str, file: str
|
|
90
|
+
) -> int:
|
|
91
|
+
ret = 1
|
|
92
|
+
with request.urlopen(
|
|
93
|
+
request.Request(
|
|
94
|
+
self.project_api_url(project)
|
|
95
|
+
+ "packages/generic/"
|
|
96
|
+
+ parse.quote_plus(package_name)
|
|
97
|
+
+ "/"
|
|
98
|
+
+ parse.quote_plus(package_version)
|
|
99
|
+
+ "/"
|
|
100
|
+
+ parse.quote(str(file)),
|
|
101
|
+
headers=self.get_headers(),
|
|
102
|
+
)
|
|
103
|
+
) as req:
|
|
104
|
+
with open(str(file), "wb") as file:
|
|
105
|
+
file.write(req.read())
|
|
106
|
+
ret = 0
|
|
107
|
+
return ret
|
|
108
|
+
|
|
109
|
+
def upload_file(
|
|
110
|
+
self, project: str, package_name: str, package_version: str, file: str
|
|
111
|
+
) -> int:
|
|
112
|
+
ret = 1
|
|
113
|
+
with open(str(file), "rb") as data:
|
|
114
|
+
res = request.urlopen(
|
|
115
|
+
request.Request(
|
|
116
|
+
self.project_api_url(project)
|
|
117
|
+
+ "packages/generic/"
|
|
118
|
+
+ parse.quote_plus(package_name)
|
|
119
|
+
+ "/"
|
|
120
|
+
+ parse.quote_plus(package_version)
|
|
121
|
+
+ "/"
|
|
122
|
+
+ parse.quote(str(file)),
|
|
123
|
+
method="PUT",
|
|
124
|
+
data=data,
|
|
125
|
+
headers=self.get_headers(),
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
if res.getcode() == 201: # 201 is created
|
|
129
|
+
ret = 0
|
|
130
|
+
return ret
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: glpkg
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Tool to make GitLab generic package registry operations easy.
|
|
5
|
+
Author-email: bugproduction <bugproduction@outlook.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://gitlab.com/bugproduction/glpkg.git
|
|
8
|
+
Keywords: GitLab,packages,registry,generic
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE.md
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: black; extra == "dev"
|
|
13
|
+
Requires-Dist: build; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
16
|
+
Requires-Dist: setuptools; extra == "dev"
|
|
17
|
+
Requires-Dist: twine; extra == "dev"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# glpkg - GitLab Generic Package registry tools
|
|
21
|
+
|
|
22
|
+
glpkg is a tool that makes it easy to work with [GitLab generic package registry](https://docs.gitlab.com/user/packages/generic_packages/).
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Install the tool from with pip:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install glpkg
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
To check the installation and version, run:
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
glpkg --version
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If you see a version in the terminal, you're good to go!
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
By default, the used GitLab host is gitlab.com. If you use a self-hosted GitLab, use argument `--host my-gitlab.net` with the commands.
|
|
45
|
+
|
|
46
|
+
> Only https scheme is supported.
|
|
47
|
+
|
|
48
|
+
To authenticate with the package registry in any of the commands below, use `--token readapitoken123` argument where the `readapitoken123` is a [personal](https://docs.gitlab.com/user/profile/personal_access_tokens/#create-a-personal-access-token) or [project](https://docs.gitlab.com/user/project/settings/project_access_tokens/#create-a-project-access-token) access token, with read API scope. In case the package registry is public, you can omit this argument.
|
|
49
|
+
|
|
50
|
+
The above arguments are omitted in the examples below to focus on the functions. Add the arguments to change the host or to authenticate with the registry.
|
|
51
|
+
|
|
52
|
+
In general, run `glpkg --help` when needed.
|
|
53
|
+
|
|
54
|
+
### Listing package versions
|
|
55
|
+
|
|
56
|
+
To list the versions of a generic package, run
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
glpkg list --project 12345 --name mypackagename
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Where:
|
|
63
|
+
- `12345` is your projects ID ([Find the Project ID](https://docs.gitlab.com/user/project/working_with_projects/#find-the-project-id)) or the path of the project (like `namespace/project`)
|
|
64
|
+
- `mypackagename` is the name of the generic package
|
|
65
|
+
|
|
66
|
+
The output will be, if package is found, something like:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
Name Version
|
|
70
|
+
mypackagename 1.0
|
|
71
|
+
mypackagename 1.5
|
|
72
|
+
mypackagename 2.0
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Download generic package
|
|
76
|
+
|
|
77
|
+
To download everything from a specific generic package, run
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
glpkg download --project 12345 --name mypackagename --version 1.0
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Where:
|
|
84
|
+
- `12345` is your projects ID ([Find the Project ID](https://docs.gitlab.com/user/project/working_with_projects/#find-the-project-id)) or the path of the project (like `namespace/project`)
|
|
85
|
+
- `mypackagename` is the name of the generic package
|
|
86
|
+
- `1.0` is the version of the generic package from which the files are downloaded
|
|
87
|
+
|
|
88
|
+
The files will be downloaded in the current working directory. Any pre-existing files will be overridden without warning.
|
|
89
|
+
|
|
90
|
+
> If a package has multiple files with the same filename, the tool can only download the newest file. This is a restriction of GitLab API.
|
|
91
|
+
|
|
92
|
+
### Upload a file to a generic package
|
|
93
|
+
|
|
94
|
+
To upload a file to a version of a generic package, run
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
glpkg upload --project 12345 --name mypackagename --version 1.0 --file my-file.txt
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Where:
|
|
101
|
+
- `12345` is your projects ID ([Find the Project ID](https://docs.gitlab.com/user/project/working_with_projects/#find-the-project-id)) or the path of the project (like `namespace/project`)
|
|
102
|
+
- `mypackagename` is the name of the generic package
|
|
103
|
+
- `1.0` is the version of the generic package to which the file is uploaded
|
|
104
|
+
- `my-file.txt` is the file that is uploaded to the generic package. Currently, only relative paths are supported, and the relative path (e.g. `folder/file.txt`) is preserved when uploading the file to the registry.
|
|
105
|
+
|
|
106
|
+
> A GitLab generic package may have multiple files with the same file name. However, it likely is not a great idea, as they cannot be downloaded separately from the GitLab API.
|
|
107
|
+
|
|
108
|
+
### Use in GitLab pipelines
|
|
109
|
+
|
|
110
|
+
If you use the tool in a GitLab pipeline, using `--ci` argument uses [GitLab predefined variables](https://docs.gitlab.com/ci/variables/predefined_variables/) to configure the tool. In this case `CI_SERVER_HOST`, `CI_PROJECT_ID`, and `CI_JOB_TOKEN` environment variables are used. The `--project`, and `--token` arguments can still be used to override the project ID and to use a personal or project access token instead of CI_JOB_TOKEN.
|
|
111
|
+
|
|
112
|
+
In other words, you don't need to give the `--host`, `--project`, or `--token` arguments if you are interacting with the package registry of the project where the pipeline is running. Example: uploading `my-file.txt` to generic package `mypackagename` version `1.0` in the project package registry in CI:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
glpkg upload --ci --name mypackagename --version 1.0 --file my-file.txt
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
To use the `CI_JOB_TOKEN` with package registry of another projects, add `--project <otherproject ID>` argument. Remember that you may need to add [permissions for the CI_JOB_TOKEN](https://docs.gitlab.com/ci/jobs/ci_job_token/#control-job-token-access-to-your-project) in the other project.
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## Limitations
|
|
122
|
+
|
|
123
|
+
The tool is not perfect (yet) and has limitations. The following limitations are known, but more can exist:
|
|
124
|
+
|
|
125
|
+
- Uploading files must be done one-by-one.
|
|
126
|
+
- Only project registries are supported for now.
|
|
127
|
+
- Pagination is not supported for now - in case you have more than 100 versions of a package, not all will be shown.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE.md
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/gitlab/__init__.py
|
|
5
|
+
src/gitlab/__main__.py
|
|
6
|
+
src/gitlab/cli_handler.py
|
|
7
|
+
src/gitlab/packages.py
|
|
8
|
+
src/glpkg.egg-info/PKG-INFO
|
|
9
|
+
src/glpkg.egg-info/SOURCES.txt
|
|
10
|
+
src/glpkg.egg-info/dependency_links.txt
|
|
11
|
+
src/glpkg.egg-info/entry_points.txt
|
|
12
|
+
src/glpkg.egg-info/requires.txt
|
|
13
|
+
src/glpkg.egg-info/top_level.txt
|
|
14
|
+
test/test_gitlab.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gitlab
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import pytest
|
|
3
|
+
import urllib
|
|
4
|
+
from gitlab.packages import *
|
|
5
|
+
from unittest.mock import mock_open, patch
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestGitLab:
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def test_gitlab(self):
|
|
11
|
+
return Packages("gl-host", "token-name", "token-value")
|
|
12
|
+
|
|
13
|
+
def test_api_url(self, test_gitlab):
|
|
14
|
+
assert test_gitlab.api_url() == "https://gl-host/api/v4/"
|
|
15
|
+
|
|
16
|
+
def test_project_api_url(self, test_gitlab):
|
|
17
|
+
assert (
|
|
18
|
+
test_gitlab.project_api_url("24") == "https://gl-host/api/v4/projects/24/"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def test_get_headers(self, test_gitlab):
|
|
22
|
+
assert test_gitlab.get_headers() == {"token-name": "token-value"}
|
|
23
|
+
|
|
24
|
+
def test_get_headers_no_name(self):
|
|
25
|
+
test_gitlab = Packages("gl-host", "", "token-value")
|
|
26
|
+
assert test_gitlab.get_headers() == {}
|
|
27
|
+
|
|
28
|
+
def test_get_headers_no_value(self):
|
|
29
|
+
test_gitlab = Packages("gl-host", "token-name", "")
|
|
30
|
+
assert test_gitlab.get_headers() == {}
|
|
31
|
+
|
|
32
|
+
def test_get_headers_no_name_no_value(self):
|
|
33
|
+
test_gitlab = Packages("gl-host", "", "")
|
|
34
|
+
assert test_gitlab.get_headers() == {}
|
|
35
|
+
|
|
36
|
+
def test_list_packages_none(self, test_gitlab):
|
|
37
|
+
data = io.StringIO("[]")
|
|
38
|
+
with patch.object(urllib.request, "urlopen", return_value=data):
|
|
39
|
+
packages = test_gitlab.list_packages("24", "package-name")
|
|
40
|
+
assert len(packages) == 0
|
|
41
|
+
|
|
42
|
+
def test_list_packages_one(self, test_gitlab):
|
|
43
|
+
data = io.StringIO('[{"name": "package-name", "version": "0.1.2"}]')
|
|
44
|
+
with patch.object(urllib.request, "urlopen", return_value=data):
|
|
45
|
+
packages = test_gitlab.list_packages("24", "package-name")
|
|
46
|
+
assert len(packages) == 1
|
|
47
|
+
|
|
48
|
+
def test_list_name_packages_filter(self, test_gitlab):
|
|
49
|
+
data = io.StringIO(
|
|
50
|
+
'[{"name": "package-name", "version": "0.1.2"}, {"name": "package-name-something", "version": "0.1.2"}]'
|
|
51
|
+
)
|
|
52
|
+
with patch.object(urllib.request, "urlopen", return_value=data):
|
|
53
|
+
packages = test_gitlab.list_packages("24", "package-name")
|
|
54
|
+
assert len(packages) == 1
|
|
55
|
+
|
|
56
|
+
def test_list_name_packages_five(self, test_gitlab):
|
|
57
|
+
data = io.StringIO(
|
|
58
|
+
'[{"name": "package-name", "version": "0.1"}, {"name": "package-name", "version": "0.2"}, {"name": "package-name", "version": "0.3"}, {"name": "package-name", "version": "0.4"}, {"name": "package-name", "version": "0.5"}]'
|
|
59
|
+
)
|
|
60
|
+
with patch.object(urllib.request, "urlopen", return_value=data):
|
|
61
|
+
packages = test_gitlab.list_packages("24", "package-name")
|
|
62
|
+
assert len(packages) == 5
|
|
63
|
+
|
|
64
|
+
def test_list_files_none(self, test_gitlab):
|
|
65
|
+
data = io.StringIO("[]")
|
|
66
|
+
with patch.object(urllib.request, "urlopen", return_value=data):
|
|
67
|
+
packages = test_gitlab.list_files("24", "123")
|
|
68
|
+
assert len(packages) == 0
|
|
69
|
+
|
|
70
|
+
def test_list_files_one(self, test_gitlab):
|
|
71
|
+
data = io.StringIO('[{"file_name": "filea.txt"}]')
|
|
72
|
+
with patch.object(urllib.request, "urlopen", return_value=data):
|
|
73
|
+
packages = test_gitlab.list_files("24", "123")
|
|
74
|
+
assert len(packages) == 1
|
|
75
|
+
|
|
76
|
+
def test_list_files_five(self, test_gitlab):
|
|
77
|
+
data = io.StringIO(
|
|
78
|
+
'[{"file_name": "filea.txt"}, {"file_name": "fileb.txt"}, {"file_name": "filec.txt"}, {"file_name": "filed.txt"}, {"file_name": "filee.txt"}]'
|
|
79
|
+
)
|
|
80
|
+
with patch.object(urllib.request, "urlopen", return_value=data):
|
|
81
|
+
packages = test_gitlab.list_files("24", "123")
|
|
82
|
+
assert len(packages) == 5
|
|
83
|
+
|
|
84
|
+
def test_package_id_none(self, test_gitlab):
|
|
85
|
+
data = io.StringIO("[]")
|
|
86
|
+
with patch.object(urllib.request, "urlopen", return_value=data):
|
|
87
|
+
packages = test_gitlab.get_package_id("24", "package-name", "0.1")
|
|
88
|
+
assert packages == 0
|
|
89
|
+
|
|
90
|
+
def test_package_id_one(self, test_gitlab):
|
|
91
|
+
data = io.StringIO('[{"id": 123}]')
|
|
92
|
+
with patch.object(urllib.request, "urlopen", return_value=data):
|
|
93
|
+
packages = test_gitlab.get_package_id("24", "package-name", "0.1")
|
|
94
|
+
assert packages == 123
|
|
95
|
+
|
|
96
|
+
def test_upload_file(self, test_gitlab):
|
|
97
|
+
class rmock:
|
|
98
|
+
def getcode():
|
|
99
|
+
return 201
|
|
100
|
+
|
|
101
|
+
with patch("builtins.open", mock_open(read_data="data")):
|
|
102
|
+
with patch.object(urllib.request, "urlopen", return_value=rmock):
|
|
103
|
+
packages = test_gitlab.upload_file("24", "package-name", "0.1", "file")
|
|
104
|
+
assert packages == 0
|
|
105
|
+
|
|
106
|
+
def test_download_file(self, test_gitlab):
|
|
107
|
+
data = io.StringIO("file-content")
|
|
108
|
+
m = mock_open()
|
|
109
|
+
with patch("builtins.open", mock_open()) as file_mock:
|
|
110
|
+
# mock_open.write.return_value = 0
|
|
111
|
+
with patch.object(urllib.request, "urlopen", return_value=data):
|
|
112
|
+
test_gitlab.download_file("24", "package-name", "0.1", "file.txt")
|
|
113
|
+
# assert ret == 0
|
|
114
|
+
file_mock.assert_called_once_with("file.txt", "wb")
|
|
115
|
+
file_mock().write.assert_called_once_with("file-content")
|