Skip to content
Open
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
38 changes: 38 additions & 0 deletions plugins/pypi/api_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package pypi

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/importer"
"github.com/1Password/shell-plugins/sdk/schema"
"github.com/1Password/shell-plugins/sdk/schema/credname"
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
)

func APIToken() schema.CredentialType {
return schema.CredentialType{
Name: credname.APIToken,
DocsURL: sdk.URL("https://pypi.org/help/#apitoken"),
ManagementURL: sdk.URL("https://pypi.org/manage/account/#api-tokens"),
Fields: []schema.CredentialField{
{
Name: fieldname.Token,
MarkdownDescription: "API token used to authenticate to PyPI.",
Secret: true,
Composition: &schema.ValueComposition{
Prefix: "pypi-",
Charset: schema.Charset{
Uppercase: true,
Lowercase: true,
Digits: true,
Specific: []rune{'-', '_'},
},
},
},
},
DefaultProvisioner: PyPIToolProvisioner("TWINE_USERNAME", "TWINE_PASSWORD"),
Importer: importer.TryAll(
importer.TryAllEnvVars(fieldname.Token, "TWINE_PASSWORD", "FLIT_PASSWORD", "HATCH_INDEX_AUTH"),
TryPyPIRCFile(),
),
}
}
96 changes: 96 additions & 0 deletions plugins/pypi/api_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package pypi

import (
"testing"

"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/plugintest"
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
)

func TestAPITokenImporter(t *testing.T) {
plugintest.TestImporter(t, APIToken().Importer, map[string]plugintest.ImportCase{
"TWINE_PASSWORD environment variable": {
Environment: map[string]string{
"TWINE_PASSWORD": "pypi-AgEIcHlwaS5vcmc",
},
ExpectedCandidates: []sdk.ImportCandidate{
{
Fields: map[sdk.FieldName]string{
fieldname.Token: "pypi-AgEIcHlwaS5vcmc",
},
},
},
},
"FLIT_PASSWORD environment variable": {
Environment: map[string]string{
"FLIT_PASSWORD": "pypi-flit123abc",
},
ExpectedCandidates: []sdk.ImportCandidate{
{
Fields: map[sdk.FieldName]string{
fieldname.Token: "pypi-flit123abc",
},
},
},
},
"HATCH_INDEX_AUTH environment variable": {
Environment: map[string]string{
"HATCH_INDEX_AUTH": "pypi-hatch789xyz",
},
ExpectedCandidates: []sdk.ImportCandidate{
{
Fields: map[sdk.FieldName]string{
fieldname.Token: "pypi-hatch789xyz",
},
},
},
},
".pypirc file with pypi section": {
Files: map[string]string{
"~/.pypirc": `[distutils]
index-servers = pypi

[pypi]
username = __token__
password = pypi-secret123`,
},
ExpectedCandidates: []sdk.ImportCandidate{
{
Fields: map[sdk.FieldName]string{
fieldname.Token: "pypi-secret123",
},
},
},
},
".pypirc file with server-login section": {
Files: map[string]string{
"~/.pypirc": `[server-login]
password = pypi-serverlogin456`,
},
ExpectedCandidates: []sdk.ImportCandidate{
{
Fields: map[sdk.FieldName]string{
fieldname.Token: "pypi-serverlogin456",
},
},
},
},
})
}

func TestAPITokenProvisioner(t *testing.T) {
plugintest.TestProvisioner(t, APIToken().DefaultProvisioner, map[string]plugintest.ProvisionCase{
"default provisioner sets TWINE env vars": {
ItemFields: map[sdk.FieldName]string{
fieldname.Token: "pypi-AgEIcHlwaS5vcmc",
},
ExpectedOutput: sdk.ProvisionOutput{
Environment: map[string]string{
"TWINE_USERNAME": "__token__",
"TWINE_PASSWORD": "pypi-AgEIcHlwaS5vcmc",
},
},
},
})
}
26 changes: 26 additions & 0 deletions plugins/pypi/flit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package pypi

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/needsauth"
"github.com/1Password/shell-plugins/sdk/schema"
"github.com/1Password/shell-plugins/sdk/schema/credname"
)

func FlitCLI() schema.Executable {
return schema.Executable{
Name: "Flit",
Runs: []string{"flit"},
DocsURL: sdk.URL("https://flit.pypa.io"),
NeedsAuth: needsauth.IfAll(
needsauth.NotForHelpOrVersion(),
needsauth.ForCommand("publish"),
),
Uses: []schema.CredentialUsage{
{
Name: credname.APIToken,
Provisioner: PyPIToolProvisioner("FLIT_USERNAME", "FLIT_PASSWORD"),
},
},
}
}
46 changes: 46 additions & 0 deletions plugins/pypi/flit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package pypi

import (
"testing"

"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/plugintest"
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
)

func TestFlitCLIProvisioner(t *testing.T) {
plugintest.TestProvisioner(t, FlitCLI().Uses[0].Provisioner, map[string]plugintest.ProvisionCase{
"sets FLIT_USERNAME and FLIT_PASSWORD": {
ItemFields: map[sdk.FieldName]string{
fieldname.Token: "pypi-flit123abc",
},
ExpectedOutput: sdk.ProvisionOutput{
Environment: map[string]string{
"FLIT_USERNAME": "__token__",
"FLIT_PASSWORD": "pypi-flit123abc",
},
},
},
})
}

func TestFlitCLINeedsAuth(t *testing.T) {
plugintest.TestNeedsAuth(t, FlitCLI().NeedsAuth, map[string]plugintest.NeedsAuthCase{
"requires auth for publish": {
Args: []string{"publish"},
ExpectedNeedsAuth: true,
},
"skips auth for help": {
Args: []string{"--help"},
ExpectedNeedsAuth: false,
},
"skips auth for version": {
Args: []string{"--version"},
ExpectedNeedsAuth: false,
},
"skips auth for build command": {
Args: []string{"build"},
ExpectedNeedsAuth: false,
},
})
}
26 changes: 26 additions & 0 deletions plugins/pypi/hatch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package pypi

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/needsauth"
"github.com/1Password/shell-plugins/sdk/schema"
"github.com/1Password/shell-plugins/sdk/schema/credname"
)

func HatchCLI() schema.Executable {
return schema.Executable{
Name: "Hatch",
Runs: []string{"hatch"},
DocsURL: sdk.URL("https://hatch.pypa.io"),
NeedsAuth: needsauth.IfAll(
needsauth.NotForHelpOrVersion(),
needsauth.ForCommand("publish"),
),
Uses: []schema.CredentialUsage{
{
Name: credname.APIToken,
Provisioner: PyPIToolProvisioner("HATCH_INDEX_USER", "HATCH_INDEX_AUTH"),
},
},
}
}
46 changes: 46 additions & 0 deletions plugins/pypi/hatch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package pypi

import (
"testing"

"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/plugintest"
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
)

func TestHatchCLIProvisioner(t *testing.T) {
plugintest.TestProvisioner(t, HatchCLI().Uses[0].Provisioner, map[string]plugintest.ProvisionCase{
"sets HATCH_INDEX_USER and HATCH_INDEX_AUTH": {
ItemFields: map[sdk.FieldName]string{
fieldname.Token: "pypi-hatch789xyz",
},
ExpectedOutput: sdk.ProvisionOutput{
Environment: map[string]string{
"HATCH_INDEX_USER": "__token__",
"HATCH_INDEX_AUTH": "pypi-hatch789xyz",
},
},
},
})
}

func TestHatchCLINeedsAuth(t *testing.T) {
plugintest.TestNeedsAuth(t, HatchCLI().NeedsAuth, map[string]plugintest.NeedsAuthCase{
"requires auth for publish": {
Args: []string{"publish"},
ExpectedNeedsAuth: true,
},
"skips auth for help": {
Args: []string{"--help"},
ExpectedNeedsAuth: false,
},
"skips auth for version": {
Args: []string{"--version"},
ExpectedNeedsAuth: false,
},
"skips auth for build command": {
Args: []string{"build"},
ExpectedNeedsAuth: false,
},
})
}
24 changes: 24 additions & 0 deletions plugins/pypi/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pypi

import (
"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/schema"
)

func New() schema.Plugin {
return schema.Plugin{
Name: "pypi",
Platform: schema.PlatformInfo{
Name: "PyPI",
Homepage: sdk.URL("https://pypi.org"),
},
Credentials: []schema.CredentialType{
APIToken(),
},
Executables: []schema.Executable{
TwineCLI(),
FlitCLI(),
HatchCLI(),
},
}
}
37 changes: 37 additions & 0 deletions plugins/pypi/provisioner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package pypi

import (
"context"
"fmt"

"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
)

// pypiToolProvisioner sets username and password environment variables
// for PyPI publishing tools. The username is always "__token__" when
// using API token authentication.
type pypiToolProvisioner struct {
usernameEnvVar string
passwordEnvVar string
}

func PyPIToolProvisioner(usernameEnvVar, passwordEnvVar string) sdk.Provisioner {
return pypiToolProvisioner{
usernameEnvVar: usernameEnvVar,
passwordEnvVar: passwordEnvVar,
}
}

func (p pypiToolProvisioner) Description() string {
return fmt.Sprintf("Provision PyPI API token via %s and %s environment variables", p.usernameEnvVar, p.passwordEnvVar)
}

func (p pypiToolProvisioner) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) {
out.AddEnvVar(p.usernameEnvVar, "__token__")
out.AddEnvVar(p.passwordEnvVar, in.ItemFields[fieldname.Token])
}

func (p pypiToolProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) {
// Environment variables are automatically cleaned up when the process ends.
}
36 changes: 36 additions & 0 deletions plugins/pypi/pypirc_importer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package pypi

import (
"context"

"github.com/1Password/shell-plugins/sdk"
"github.com/1Password/shell-plugins/sdk/importer"
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
)

func TryPyPIRCFile() sdk.Importer {
return importer.TryFile("~/.pypirc", func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) {
cfg, err := contents.ToINI()
if err != nil {
return
}

// Try [pypi] section first, then [server-login]
for _, section := range []string{"pypi", "server-login"} {
s := cfg.Section(section)
if s == nil {
continue
}

password := s.Key("password").String()
if password != "" {
out.AddCandidate(sdk.ImportCandidate{
Fields: map[sdk.FieldName]string{
fieldname.Token: password,
},
})
return
}
}
})
}
Loading