Merge lp:~jtv/launchpad/redo-uploads into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~jtv/launchpad/redo-uploads
Merge into: lp:launchpad
Diff against target: 449 lines
6 files modified
lib/lp/registry/interfaces/sourcepackage.py (+7/-0)
lib/lp/registry/model/sourcepackage.py (+33/-0)
lib/lp/soyuz/doc/distroseriesqueue-translations.txt (+43/-0)
lib/lp/translations/scripts/reupload_translations.py (+110/-0)
lib/lp/translations/scripts/tests/test_reupload_translations.py (+168/-0)
scripts/rosetta/reupload-translations.py (+19/-0)
To merge this branch: bzr merge lp:~jtv/launchpad/redo-uploads
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Abstain
Edwin Grubbs (community) code Approve
Review via email: mp+12659@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

= Bug 439346 =

A stupid missing parameter in 3.0 caused the translations auto-approver
to approve uploads for the right packages but the wrong Ubuntu releases.
Due to some other bugs that we've fixed already, we don't know exactly
which translations will or won't be affected. So we ended up with a
fairly long list of packages that _may_ have had the wrong files
imported to them.

This branch provides a script that finds the latest Soyuz-generated
translations upload for a given package (a tarball containing, roughly
speaking, the upstream templates and translations) and re-uploads it.

Finding the latest translations upload for a package turns out to be...
in Julian's words, "easy." I'd probably pick another word myself.
After conferring with Celso (who helped me get it actually working) I
isolated that piece of work in SourcePackage.

You may wonder why the script keeps track of uploadless_packages. That
is there just to facilitate testing.

There is no single end-to-end test. It's just too hard to set up and
maintain a realistic testing situation for this. Test coverage consists
of several stretches that touch more than overlap:

 * A full script run, but without any uploads being found.
   This exercises everything except retrieving and uploading a tarball
   from the Librarian, and the tail end of getLatestTranslations in the
   success case.

 * A before-and-after demonstration of getLatestTranslationsUploads in
   the doctest. This does exercise the tail end in the success case.
   It also shows that a gzipped tarball containing the right files has
   gone into the Librarian.

 * A unit test for _findPackage, to cover up for the fact that other
   unit tests patch that method for mocking purposes.

 * A successful run of the LaunchpadScript object, but with
   getLatestTranslationsUploads mocked up to return a gzipped tarball
   that was stuffed straight into the Librarian by the test. This also
   tests the retrieval and uploading of that tarball.

The tar-file handling in the tests is terrible. I can't really help it;
blame the tarfile module's horrible API. Makes it hard to create
tarballs in-memory using StringIOs.

== Tests ==
{{{
./bin/test -vv -t distroseriesqueue-translations.txt
./bin/test -vv -t reupload_translations
}}}

== Demo and Q/A ==

Pick a source package in "main" for some Ubuntu release, one with
translations but not too much in its import queue—there has to be at
least one translation that doesn't have a Needs Review or Approved entry
in the queue. You can create a new translation by translating a string
into a dead language such as Sumerian (sux).

Run the script, passing it the name of the Ubuntu release (e.g. karmic)
and the name of the source package (e.g. tomboy). Try the --dry-run
option first; nothing will actually happen to the package's import
queue. Try the same run again without the --dry-run option, and now a
full set of upstream translations will appear on the queue in Needs
Review state.

Or if you wait too long, Approved or even Imported. The approval and
import scripts do run on staging.

No lint.

Jeroen

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (10.7 KiB)

Hi Jeroen,

This looks good. I just have a few formatting comments.

merge-conditional

-Edwin

>=== added file 'lib/lp/translations/scripts/reupload_translations.py'
>--- lib/lp/translations/scripts/reupload_translations.py 1970-01-01 00:00:00 +0000
>+++ lib/lp/translations/scripts/reupload_translations.py 2009-09-30 15:42:55 +0000
>@@ -0,0 +1,105 @@
>+__metaclass__ = type

Missing copyright notice.

>+
>+__all__ = [
>+ 'ReuploadPackageTranslations',
>+ ]

Indentation should be spaces.

>+
>+from zope.component import getUtility
>+
>+from lp.services.scripts.base import LaunchpadScript, LaunchpadScriptFailure
>+
>+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
>+from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
>+from lp.translations.interfaces.translationimportqueue import (
>+ ITranslationImportQueue)
>+
>+
>+class ReuploadPackageTranslations(LaunchpadScript):
>+ """Re-upload latest translations for given distribution packages."""
>+ description = "Re-upload latest translations uploads for package(s)."
>+
>+ def add_my_options(self):
>+ """See `LaunchpadScript`."""
>+ self.parser.add_option('-d', '--distribution', dest='distro',
>+ help="Distribution to upload for.", default='ubuntu')
>+ self.parser.add_option('-s', '--series', dest='distroseries',
>+ help="Distribution release series to upload for.")
>+ self.parser.add_option('-p', '--package', action='append',
>+ dest='packages', default=[],
>+ help="Name(s) of source package(s) to re-upload.")
>+ self.parser.add_option('-l', '--dry-run', dest='dryrun',
>+ action='store_true', default=False,
>+ help="Pretend to upload, but make no actual changes.")
>+

Trailing white space.

>+ def main(self):
>+ """See `LaunchpadScript`."""
>+ self.uploadless_packages = []
>+ self._setDistroDetails()
>+
>+ if len(self.options.packages) == 0:
>+ raise LaunchpadScriptFailure("No packages specified.")
>+
>+ if self.options.dryrun:
>+ self.logger.info("Dry run. Not really uploading anything.")
>+
>+ for package_name in self.options.packages:
>+ self._processPackage(self._findPackage(package_name))
>+ self._commit()
>+
>+ self.logger.info("Done.")
>+
>+ def _commit(self):
>+ """Commit transaction (or abort if dry run)."""
>+ if self.txn:
>+ if self.options.dryrun:
>+ self.txn.abort()
>+ else:
>+ self.txn.commit()
>+
>+ def _setDistroDetails(self):
>+ """Figure out the `Distribution`/`DistroSeries` to act upon."""
>+ # Avoid circular imports.
>+ from lp.registry.interfaces.distribution import IDistributionSet
>+
>+ distroset = getUtility(IDistributionSet)
>+ self.distro = distroset.getByName(self.options.distro)
>+
>+ if not self.options.distroseries:
>+ raise LaunchpadScriptFailure(
>+ "Specify a distribution release series.")
>+
>+ self.distroseries = self.distro.getSeries(self.options.distroserie...

review: Approve (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> Hi Jeroen,
>
> This looks good. I just have a few formatting comments.

Thanks. Whitespace problems all fixed.

"I have no idea how that tab got into my code, officer."

review: Abstain

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/interfaces/sourcepackage.py'
--- lib/lp/registry/interfaces/sourcepackage.py 2009-09-16 04:31:39 +0000
+++ lib/lp/registry/interfaces/sourcepackage.py 2009-09-30 16:53:17 +0000
@@ -225,6 +225,13 @@
225 title=u'The component in which the package was last published.',225 title=u'The component in which the package was last published.',
226 schema=IComponent, readonly=True, required=False)226 schema=IComponent, readonly=True, required=False)
227227
228 def getLatestTranslationsUploads():
229 """Find latest Translations tarballs as produced by Soyuz.
230
231 :return: A list of `ILibraryFileAlias`es, usually of size zero
232 or one. If not, they are sorted from oldest to newest.
233 """
234
228235
229class ISourcePackageFactory(Interface):236class ISourcePackageFactory(Interface):
230 """A creator of source packages."""237 """A creator of source packages."""
231238
=== modified file 'lib/lp/registry/model/sourcepackage.py'
--- lib/lp/registry/model/sourcepackage.py 2009-09-25 17:00:20 +0000
+++ lib/lp/registry/model/sourcepackage.py 2009-09-30 16:53:17 +0000
@@ -54,6 +54,7 @@
54from lp.translations.interfaces.potemplate import IHasTranslationTemplates54from lp.translations.interfaces.potemplate import IHasTranslationTemplates
55from lp.registry.interfaces.pocket import PackagePublishingPocket55from lp.registry.interfaces.pocket import PackagePublishingPocket
56from lp.soyuz.interfaces.publishing import PackagePublishingStatus56from lp.soyuz.interfaces.publishing import PackagePublishingStatus
57from lp.soyuz.interfaces.queue import PackageUploadCustomFormat
57from lp.answers.interfaces.questioncollection import (58from lp.answers.interfaces.questioncollection import (
58 QUESTION_STATUS_DEFAULT_SEARCH)59 QUESTION_STATUS_DEFAULT_SEARCH)
59from lp.answers.interfaces.questiontarget import IQuestionTarget60from lp.answers.interfaces.questiontarget import IQuestionTarget
@@ -655,3 +656,35 @@
655 self.distribution.name,656 self.distribution.name,
656 self.distroseries.getSuite(pocket),657 self.distroseries.getSuite(pocket),
657 self.name)658 self.name)
659
660 def getLatestTranslationsUploads(self):
661 """See `ISourcePackage`."""
662 our_format = PackageUploadCustomFormat.ROSETTA_TRANSLATIONS
663
664 packagename = self.sourcepackagename.name
665 displayname = self.displayname
666 distro = self.distroseries.distribution
667
668 histories = distro.main_archive.getPublishedSources(
669 name=packagename, distroseries=self.distroseries,
670 status=PackagePublishingStatus.PUBLISHED, exact_match=True)
671 histories = list(histories)
672
673 builds = []
674 for history in histories:
675 builds += list(history.getBuilds())
676
677 uploads = [
678 build.package_upload
679 for build in builds
680 if build.package_upload
681 ]
682 custom_files = []
683 for upload in uploads:
684 custom_files += [
685 custom for custom in upload.customfiles
686 if custom.customformat == our_format
687 ]
688
689 custom_files.sort(key=attrgetter('id'))
690 return [custom.libraryfilealias for custom in custom_files]
658691
=== modified file 'lib/lp/soyuz/doc/distroseriesqueue-translations.txt'
--- lib/lp/soyuz/doc/distroseriesqueue-translations.txt 2009-09-04 08:35:20 +0000
+++ lib/lp/soyuz/doc/distroseriesqueue-translations.txt 2009-09-30 16:53:17 +0000
@@ -97,6 +97,14 @@
97 >>> pmount_upload.is_rejected97 >>> pmount_upload.is_rejected
98 False98 False
9999
100At this point, no translations uploads have been registered for this
101package.
102
103 >>> from lp.registry.model.sourcepackage import SourcePackage
104 >>> dapper_pmount = SourcePackage(pmount_sourcepackagename, dapper)
105 >>> print len(dapper_pmount.getLatestTranslationsUploads())
106 0
107
100 >>> success = pmount_upload.do_accept()108 >>> success = pmount_upload.do_accept()
101 DEBUG: Creating queue entry109 DEBUG: Creating queue entry
102 DEBUG: Build ... found110 DEBUG: Build ... found
@@ -115,6 +123,17 @@
115 #NEW: pmount_0.9.7-2ubuntu2_amd64.deb123 #NEW: pmount_0.9.7-2ubuntu2_amd64.deb
116 #OK: pmount_0.9.7-2ubuntu2_amd64_translations.tar.gz124 #OK: pmount_0.9.7-2ubuntu2_amd64_translations.tar.gz
117125
126The upload now shows up as the latest translations upload for the
127package.
128
129 >>> latest_translations_uploads = list(
130 ... dapper_pmount.getLatestTranslationsUploads())
131 >>> print len(latest_translations_uploads)
132 1
133
134We'll get back to that uploaded file later.
135
136 >>> latest_translations_upload = latest_translations_uploads[0]
118137
119 # Check the import queue content, it should be empty.138 # Check the import queue content, it should be empty.
120 >>> from lp.translations.interfaces.translationimportqueue import (139 >>> from lp.translations.interfaces.translationimportqueue import (
@@ -259,6 +278,7 @@
259 # component.278 # component.
260 >>> transaction.abort()279 >>> transaction.abort()
261280
281
262== Translations from PPA build ==282== Translations from PPA build ==
263283
264For now we simply ignore translations for archives other than the284For now we simply ignore translations for archives other than the
@@ -376,3 +396,26 @@
376 >>> translations_upload.packageupload = carlos_package_upload396 >>> translations_upload.packageupload = carlos_package_upload
377 >>> translations_upload.publish_ROSETTA_TRANSLATIONS()397 >>> translations_upload.publish_ROSETTA_TRANSLATIONS()
378 Imported by: carlos398 Imported by: carlos
399
400
401=== Translations tarball ===
402
403The LibraryFileAlias returned by getLatestTranslationsUploads on the
404source package points to a tarball with translations files for the
405package.
406
407 >>> import tarfile
408 >>> from StringIO import StringIO
409 >>> tarball = StringIO(latest_translations_upload.read())
410 >>> archive = tarfile.open('', 'r|gz', tarball)
411 >>> translation_files = sorted([
412 ... entry.name for entry in archive.getmembers()
413 ... if entry.name.endswith('.po') or entry.name.endswith('.pot')
414 ... ])
415 >>> for filename in translation_files:
416 ... print filename
417 source/po/ca.po
418 source/po/cs.po
419 source/po/de.po
420 ...
421 source/po/pmount.pot
379422
=== added file 'lib/lp/translations/scripts/reupload_translations.py'
--- lib/lp/translations/scripts/reupload_translations.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/scripts/reupload_translations.py 2009-09-30 16:53:17 +0000
@@ -0,0 +1,110 @@
1#! /usr/bin/python2.4
2#
3# Copyright 2009 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6__metaclass__ = type
7
8__all__ = [
9 'ReuploadPackageTranslations',
10 ]
11
12from zope.component import getUtility
13
14from lp.services.scripts.base import LaunchpadScript, LaunchpadScriptFailure
15
16from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
17from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
18from lp.translations.interfaces.translationimportqueue import (
19 ITranslationImportQueue)
20
21
22class ReuploadPackageTranslations(LaunchpadScript):
23 """Re-upload latest translations for given distribution packages."""
24 description = "Re-upload latest translations uploads for package(s)."
25
26 def add_my_options(self):
27 """See `LaunchpadScript`."""
28 self.parser.add_option('-d', '--distribution', dest='distro',
29 help="Distribution to upload for.", default='ubuntu')
30 self.parser.add_option('-s', '--series', dest='distroseries',
31 help="Distribution release series to upload for.")
32 self.parser.add_option('-p', '--package', action='append',
33 dest='packages', default=[],
34 help="Name(s) of source package(s) to re-upload.")
35 self.parser.add_option('-l', '--dry-run', dest='dryrun',
36 action='store_true', default=False,
37 help="Pretend to upload, but make no actual changes.")
38
39 def main(self):
40 """See `LaunchpadScript`."""
41 self.uploadless_packages = []
42 self._setDistroDetails()
43
44 if len(self.options.packages) == 0:
45 raise LaunchpadScriptFailure("No packages specified.")
46
47 if self.options.dryrun:
48 self.logger.info("Dry run. Not really uploading anything.")
49
50 for package_name in self.options.packages:
51 self._processPackage(self._findPackage(package_name))
52 self._commit()
53
54 self.logger.info("Done.")
55
56 def _commit(self):
57 """Commit transaction (or abort if dry run)."""
58 if self.txn:
59 if self.options.dryrun:
60 self.txn.abort()
61 else:
62 self.txn.commit()
63
64 def _setDistroDetails(self):
65 """Figure out the `Distribution`/`DistroSeries` to act upon."""
66 # Avoid circular imports.
67 from lp.registry.interfaces.distribution import IDistributionSet
68
69 distroset = getUtility(IDistributionSet)
70 self.distro = distroset.getByName(self.options.distro)
71
72 if not self.options.distroseries:
73 raise LaunchpadScriptFailure(
74 "Specify a distribution release series.")
75
76 self.distroseries = self.distro.getSeries(self.options.distroseries)
77
78 def _findPackage(self, name):
79 """Find `SourcePackage` of given name."""
80 # Avoid circular imports.
81 from lp.registry.interfaces.sourcepackage import ISourcePackageFactory
82
83 factory = getUtility(ISourcePackageFactory)
84 nameset = getUtility(ISourcePackageNameSet)
85
86 sourcepackagename = nameset.queryByName(name)
87
88 return factory.new(sourcepackagename, self.distroseries)
89
90 def _processPackage(self, package):
91 """Get translations for `package` re-uploaded."""
92 self.logger.info("Processing %s" % package.displayname)
93 tarball_aliases = package.getLatestTranslationsUploads()
94 queue = getUtility(ITranslationImportQueue)
95 rosetta_team = getUtility(ILaunchpadCelebrities).rosetta_experts
96
97 have_uploads = False
98 for alias in tarball_aliases:
99 have_uploads = True
100 self.logger.debug("Uploading file '%s' for %s." % (
101 alias.filename, package.displayname))
102 queue.addOrUpdateEntriesFromTarball(
103 alias.read(), True, rosetta_team,
104 sourcepackagename=package.sourcepackagename,
105 distroseries=self.distroseries)
106
107 if not have_uploads:
108 self.logger.warn(
109 "Found no translations upload for %s." % package.displayname)
110 self.uploadless_packages.append(package)
0111
=== added file 'lib/lp/translations/scripts/tests/test_reupload_translations.py'
--- lib/lp/translations/scripts/tests/test_reupload_translations.py 1970-01-01 00:00:00 +0000
+++ lib/lp/translations/scripts/tests/test_reupload_translations.py 2009-09-30 16:53:17 +0000
@@ -0,0 +1,168 @@
1#! /usr/bin/python2.4
2#
3# Copyright 2009 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5
6"""Test `reupload_translations` and `ReuploadPackageTranslations`."""
7
8__metaclass__ = type
9
10from unittest import TestLoader
11
12import re
13from StringIO import StringIO
14import tarfile
15import transaction
16
17from zope.security.proxy import removeSecurityProxy
18
19from canonical.testing import LaunchpadZopelessLayer
20from lp.testing import TestCaseWithFactory
21from canonical.launchpad.scripts.tests import run_script
22
23from canonical.launchpad.database.librarian import LibraryFileAliasSet
24from lp.registry.model.sourcepackage import SourcePackage
25from lp.translations.model.translationimportqueue import (
26 TranslationImportQueue)
27
28from lp.translations.scripts.reupload_translations import (
29 ReuploadPackageTranslations)
30
31
32class UploadInjector:
33 def __init__(self, script, tar_alias):
34 self.tar_alias = tar_alias
35 self.script = script
36 self.original_findPackage = script._findPackage
37
38 def __call__(self, name):
39 package = self.original_findPackage(name)
40 removeSecurityProxy(package).getLatestTranslationsUploads = (
41 self._fakeTranslationsUpload)
42 return package
43
44 def _fakeTranslationsUpload(self):
45 return [self.tar_alias]
46
47
48def upload_tarball(translation_files):
49 """Create a tarball and upload it to the Librarian.
50
51 :param translation_files: A dict mapping filenames to file contents.
52 :return: A `LibraryFileAlias`.
53 """
54 buf = StringIO()
55 tarball = tarfile.open('', 'w:gz', buf)
56 for name, contents in translation_files.iteritems():
57 pseudofile = StringIO(contents)
58 tarinfo = tarfile.TarInfo()
59 tarinfo.name = name
60 tarinfo.size = len(contents)
61 tarinfo.type = tarfile.REGTYPE
62 tarball.addfile(tarinfo, pseudofile)
63
64 tarball.close()
65 buf.flush()
66 tarsize = buf.tell()
67 buf.seek(0)
68
69 return LibraryFileAliasSet().create(
70 'uploads.tar.gz', tarsize, buf, 'application/x-gtar')
71
72
73def summarize_translations_queue(sourcepackage):
74 """Describe queue entries for `sourcepackage` as a name/contents dict."""
75 entries = TranslationImportQueue().getAllEntries(sourcepackage)
76 return dict((entry.path, entry.content.read()) for entry in entries)
77
78
79class TestReuploadPackageTranslations(TestCaseWithFactory):
80 """Test `ReuploadPackageTranslations`."""
81 layer = LaunchpadZopelessLayer
82
83 def setUp(self):
84 super(TestReuploadPackageTranslations, self).setUp()
85 sourcepackagename = self.factory.makeSourcePackageName()
86 distroseries = self.factory.makeDistroRelease()
87 self.sourcepackage = SourcePackage(sourcepackagename, distroseries)
88 self.script = ReuploadPackageTranslations('reupload', test_args=[
89 '-d', distroseries.distribution.name,
90 '-s', distroseries.name,
91 '-p', sourcepackagename.name,
92 '-qqq'])
93
94 def test_findPackage(self):
95 # _findPackage finds a SourcePackage by name.
96 self.script._setDistroDetails()
97 found_package = self.script._findPackage(
98 self.sourcepackage.sourcepackagename.name)
99 self.assertEqual(self.sourcepackage, found_package)
100
101 def test_processPackage_nothing(self):
102 # A package need not have a translations upload. The script
103 # notices this but does nothing about it.
104 self.script.main()
105 self.assertEqual(
106 [self.sourcepackage], self.script.uploadless_packages)
107
108 def test_processPackage(self):
109 # _processPackage will fetch the package's latest translations
110 # upload from the Librarian and re-import it.
111 translation_files = {
112 'po/messages.pot': '# pot',
113 'po/nl.po': '# nl',
114 }
115 tar_alias = upload_tarball(translation_files)
116
117 # Force Librarian update
118 transaction.commit()
119
120 self.script._findPackage = UploadInjector(self.script, tar_alias)
121 self.script.main()
122 self.assertEqual([], self.script.uploadless_packages)
123
124 # Force Librarian update
125 transaction.commit()
126
127 queue_summary = summarize_translations_queue(self.sourcepackage)
128 self.assertEqual(translation_files, queue_summary)
129
130
131class TestReuploadScript(TestCaseWithFactory):
132 """Test reupload-translations script."""
133 layer = LaunchpadZopelessLayer
134
135 def setUp(self):
136 super(TestReuploadScript, self).setUp()
137 self.distroseries = self.factory.makeDistroRelease()
138 self.sourcepackagename1 = self.factory.makeSourcePackageName()
139 self.sourcepackagename2 = self.factory.makeSourcePackageName()
140 transaction.commit()
141
142 def test_reupload_translations(self):
143 """Test a run of the script."""
144 retcode, stdout, stderr = run_script(
145 'scripts/rosetta/reupload-translations.py', [
146 '-d', self.distroseries.distribution.name,
147 '-s', self.distroseries.name,
148 '-p', self.sourcepackagename1.name,
149 '-p', self.sourcepackagename2.name,
150 '-vvv',
151 '--dry-run',
152 ])
153
154 self.assertEqual(0, retcode)
155 self.assertEqual('', stdout)
156
157 expected_output = (
158 "INFO\s*Dry run. Not really uploading anything.\n"
159 "INFO\s*Processing [^\s]+ in .*\n"
160 "WARNING\s*Found no translations upload for .*\n"
161 "INFO\s*Processing [^\s]+ in .*\n"
162 "WARNING\s*Found no translations upload for .*\n"
163 "INFO\s*Done.\n")
164 self.assertTrue(re.match(expected_output, stderr))
165
166
167def test_suite():
168 return TestLoader().loadTestsFromName(__name__)
0169
=== added file 'scripts/rosetta/reupload-translations.py'
--- scripts/rosetta/reupload-translations.py 1970-01-01 00:00:00 +0000
+++ scripts/rosetta/reupload-translations.py 2009-09-30 16:53:17 +0000
@@ -0,0 +1,19 @@
1#! /usr/bin/env python2.4
2#
3# Copyright 2009 Canonical Ltd. This software is licensed under the
4# GNU Affero General Public License version 3 (see the file LICENSE).
5# pylint: disable-msg=W0403
6
7"""Re-upload translations from given packages."""
8
9__metaclass__ = type
10
11import _pythonpath
12
13from lp.translations.scripts.reupload_translations import (
14 ReuploadPackageTranslations)
15
16
17if __name__ == '__main__':
18 script = ReuploadPackageTranslations('reupload-translations')
19 script.run()